| """ |
| AgentFile Model Merger - Advanced MoE Beyond Normal |
| Uses HuggingFace Transformers for model merging |
| Supports GGUF, SafeTensors, and HuggingFace Hub models |
| """ |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from transformers import ( |
| AutoModelForCausalLM, |
| AutoTokenizer, |
| AutoConfig, |
| BitsAndBytesConfig |
| ) |
| from typing import Dict, List, Optional, Tuple, Union |
| import numpy as np |
| from dataclasses import dataclass, field |
| from enum import Enum |
| import json |
| import os |
| import sys |
| import logging |
| from pathlib import Path |
| import gc |
| import time |
|
|
| |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') |
| logger = logging.getLogger(__name__) |
|
|
| class MergeStrategy(Enum): |
| """Advanced merge strategies beyond normal MoE""" |
| TIES = "ties" |
| DARE = "dare" |
| MODEL_SOUP = "model_soup" |
| DEEP_MERGE = "deep_merge" |
| ADAPTIVE_FUSION = "adaptive_fusion" |
| NEURAL_SYNTHESIS = "neural_synthesis" |
|
|
| @dataclass |
| class ExpertConfig: |
| """Configuration for an expert model""" |
| name: str |
| path: str |
| weight: float = 1.0 |
| specialization: str = "general" |
| memory_requirement: float = 1.0 |
| compute_requirement: float = 1.0 |
| device_map: str = "auto" |
| torch_dtype: str = "float16" |
| load_in_4bit: bool = False |
| load_in_8bit: bool = False |
|
|
| @dataclass |
| class MergedModelConfig: |
| """Configuration for the merged model""" |
| experts: List[ExpertConfig] = field(default_factory=list) |
| merge_strategy: MergeStrategy = MergeStrategy.ADAPTIVE_FUSION |
| router_type: str = "neural_router" |
| max_experts_per_token: int = 4 |
| load_balancing_factor: float = 0.1 |
| memory_budget: float = 8.0 |
| use_dynamic_routing: bool = True |
| quality_threshold: float = 0.8 |
| output_path: str = "models/merged_model" |
| push_to_hub: bool = False |
| hub_model_id: Optional[str] = None |
|
|
| class HuggingFaceModelLoader: |
| """Handles loading models from HuggingFace Hub or local paths""" |
| |
| def __init__(self): |
| self.loaded_models = {} |
| self.loaded_tokenizers = {} |
| |
| def load_model( |
| self, |
| model_path: str, |
| device_map: str = "auto", |
| torch_dtype: str = "float16", |
| load_in_4bit: bool = False, |
| load_in_8bit: bool = False |
| ) -> Tuple[AutoModelForCausalLM, AutoTokenizer]: |
| """Load model and tokenizer from HuggingFace or local path""" |
| |
| if model_path in self.loaded_models: |
| logger.info(f"Model already loaded: {model_path}") |
| return self.loaded_models[model_path], self.loaded_tokenizers[model_path] |
| |
| logger.info(f"Loading model: {model_path}") |
| start_time = time.time() |
| |
| try: |
| |
| dtype_map = { |
| "float16": torch.float16, |
| "bfloat16": torch.bfloat16, |
| "float32": torch.float32 |
| } |
| dtype = dtype_map.get(torch_dtype, torch.float16) |
| |
| |
| quantization_config = None |
| if load_in_4bit: |
| quantization_config = BitsAndBytesConfig( |
| load_in_4bit=True, |
| bnb_4bit_compute_dtype=dtype, |
| bnb_4bit_use_double_quant=True, |
| bnb_4bit_quant_type="nf4" |
| ) |
| elif load_in_8bit: |
| quantization_config = BitsAndBytesConfig( |
| load_in_8bit=True |
| ) |
| |
| |
| tokenizer = AutoTokenizer.from_pretrained( |
| model_path, |
| trust_remote_code=True |
| ) |
| |
| |
| model_kwargs = { |
| "pretrained_model_name_or_path": model_path, |
| "device_map": device_map, |
| "torch_dtype": dtype, |
| "trust_remote_code": True |
| } |
| |
| if quantization_config: |
| model_kwargs["quantization_config"] = quantization_config |
| |
| model = AutoModelForCausalLM.from_pretrained(**model_kwargs) |
| |
| |
| self.loaded_models[model_path] = model |
| self.loaded_tokenizers[model_path] = tokenizer |
| |
| load_time = time.time() - start_time |
| logger.info(f"Model loaded successfully in {load_time:.2f}s") |
| |
| return model, tokenizer |
| |
| except Exception as e: |
| logger.error(f"Failed to load model {model_path}: {e}") |
| raise |
| |
| def unload_model(self, model_path: str): |
| """Unload a model to free memory""" |
| if model_path in self.loaded_models: |
| del self.loaded_models[model_path] |
| del self.loaded_tokenizers[model_path] |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
| logger.info(f"Model unloaded: {model_path}") |
| |
| def get_model_info(self, model_path: str) -> Dict: |
| """Get model information without loading it""" |
| try: |
| config = AutoConfig.from_pretrained(model_path, trust_remote_code=True) |
| return { |
| "hidden_size": config.hidden_size, |
| "num_hidden_layers": config.num_hidden_layers, |
| "num_attention_heads": config.num_attention_heads, |
| "vocab_size": config.vocab_size, |
| "model_type": config.model_type |
| } |
| except Exception as e: |
| logger.warning(f"Could not get model info for {model_path}: {e}") |
| return {} |
|
|
| class DeepMerger: |
| """Advanced Deep Merger - Goes beyond normal weight averaging""" |
| |
| def __init__(self, strategy: MergeStrategy): |
| self.strategy = strategy |
| |
| def merge_models( |
| self, |
| models: List[AutoModelForCausalLM], |
| weights: List[float], |
| config: MergedModelConfig |
| ) -> AutoModelForCausalLM: |
| """Merge multiple models into one""" |
| |
| logger.info(f"Merging {len(models)} models using {self.strategy.value} strategy") |
| |
| if self.strategy == MergeStrategy.TIES: |
| return self._ties_merge(models, weights) |
| elif self.strategy == MergeStrategy.DARE: |
| return self._dare_merge(models, weights) |
| elif self.strategy == MergeStrategy.DEEP_MERGE: |
| return self._deep_merge(models, weights) |
| elif self.strategy == MergeStrategy.ADAPTIVE_FUSION: |
| return self._adaptive_fusion_merge(models, weights) |
| elif self.strategy == MergeStrategy.NEURAL_SYNTHESIS: |
| return self._neural_synthesis_merge(models, weights) |
| else: |
| return self._model_soup_merge(models, weights) |
| |
| def _ties_merge( |
| self, |
| models: List[AutoModelForCausalLM], |
| weights: List[float] |
| ) -> AutoModelForCausalLM: |
| """TIES merging - Task Interpolation with Exponential Smoothing""" |
| |
| logger.info("Applying TIES merging...") |
| |
| |
| merged_model = models[0] |
| |
| |
| param_keys = list(merged_model.state_dict().keys()) |
| |
| |
| diffs = [] |
| for model in models[1:]: |
| diff = {} |
| for key in param_keys: |
| diff[key] = model.state_dict()[key] - merged_model.state_dict()[key] |
| diffs.append(diff) |
| |
| |
| merged_params = {} |
| for key in param_keys: |
| |
| values = [merged_model.state_dict()[key]] |
| for diff in diffs: |
| values.append(merged_model.state_dict()[key] + diff[key]) |
| |
| |
| smoothed = values[0] |
| for i, val in enumerate(values[1:], 1): |
| alpha = weights[i] / sum(weights) |
| smoothed = smoothed * (1 - alpha) + val * alpha |
| |
| merged_params[key] = smoothed |
| |
| |
| merged_model.load_state_dict(merged_params) |
| |
| return merged_model |
| |
| def _dare_merge( |
| self, |
| models: List[AutoModelForCausalLM], |
| weights: List[float] |
| ) -> AutoModelForCausalLM: |
| """DARE merging - Drop And REscale""" |
| |
| logger.info("Applying DARE merging...") |
| |
| merged_model = models[0] |
| param_keys = list(merged_model.state_dict().keys()) |
| |
| |
| importance_scores = {} |
| for key in param_keys: |
| values = [model.state_dict()[key] for model in models] |
| variance = torch.var(torch.stack([v.float() for v in values]), dim=0) |
| importance_scores[key] = variance |
| |
| |
| merged_params = {} |
| for key in param_keys: |
| |
| inv_importance = 1.0 / (importance_scores[key] + 1e-10) |
| inv_importance = inv_importance / inv_importance.sum() |
| |
| weighted_sum = torch.zeros_like(models[0].state_dict()[key], dtype=torch.float32) |
| for i, model in enumerate(models): |
| weighted_sum += weights[i] * model.state_dict()[key].float() * inv_importance |
| |
| merged_params[key] = weighted_sum.to(models[0].state_dict()[key].dtype) |
| |
| merged_model.load_state_dict(merged_params) |
| return merged_model |
| |
| def _deep_merge( |
| self, |
| models: List[AutoModelForCausalLM], |
| weights: List[float] |
| ) -> AutoModelForCausalLM: |
| """Deep layer-wise merging - Analyzes and merges each layer differently""" |
| |
| logger.info("Applying deep layer-wise merging...") |
| |
| merged_model = models[0] |
| param_keys = list(merged_model.state_dict().keys()) |
| |
| |
| layer_groups = {} |
| for key in param_keys: |
| parts = key.split('.') |
| layer_num = None |
| for part in parts: |
| if part.isdigit(): |
| layer_num = int(part) |
| break |
| |
| if layer_num is not None: |
| if layer_num not in layer_groups: |
| layer_groups[layer_num] = [] |
| layer_groups[layer_num].append(key) |
| else: |
| |
| if 'global' not in layer_groups: |
| layer_groups['global'] = [] |
| layer_groups['global'].append(key) |
| |
| |
| merged_params = {} |
| for layer_num, keys in layer_groups.items(): |
| if layer_num == 'global': |
| |
| for key in keys: |
| weighted_sum = torch.zeros_like(models[0].state_dict()[key], dtype=torch.float32) |
| for i, model in enumerate(models): |
| weighted_sum += weights[i] * model.state_dict()[key].float() |
| merged_params[key] = weighted_sum.to(models[0].state_dict()[key].dtype) |
| else: |
| |
| layer_complexity = self._analyze_layer_complexity(models, keys) |
| |
| for key in keys: |
| if layer_complexity > 0.7: |
| |
| values = [model.state_dict()[key] for model in models] |
| smoothed = values[0] |
| for i, val in enumerate(values[1:], 1): |
| alpha = weights[i] / sum(weights) |
| smoothed = smoothed * (1 - alpha) + val * alpha |
| merged_params[key] = smoothed |
| else: |
| |
| weighted_sum = torch.zeros_like(models[0].state_dict()[key], dtype=torch.float32) |
| for i, model in enumerate(models): |
| weighted_sum += weights[i] * model.state_dict()[key].float() |
| merged_params[key] = weighted_sum.to(models[0].state_dict()[key].dtype) |
| |
| merged_model.load_state_dict(merged_params) |
| return merged_model |
| |
| def _adaptive_fusion_merge( |
| self, |
| models: List[AutoModelForCausalLM], |
| weights: List[float] |
| ) -> AutoModelForCausalLM: |
| """Adaptive Fusion - Dynamically adjusts merging based on input""" |
| |
| logger.info("Applying adaptive fusion merging...") |
| |
| merged_model = models[0] |
| param_keys = list(merged_model.state_dict().keys()) |
| |
| |
| fusion_gates = {} |
| for key in param_keys: |
| shape = models[0].state_dict()[key].shape |
| gate = torch.ones(len(models), *shape, dtype=torch.float32) / len(models) |
| fusion_gates[key] = gate |
| |
| |
| merged_params = {} |
| for key in param_keys: |
| weighted_sum = torch.zeros_like(models[0].state_dict()[key], dtype=torch.float32) |
| |
| for i, model in enumerate(models): |
| gate = fusion_gates[key][i] |
| weighted_sum += gate * weights[i] * model.state_dict()[key].float() |
| |
| merged_params[key] = weighted_sum.to(models[0].state_dict()[key].dtype) |
| |
| merged_model.load_state_dict(merged_params) |
| |
| |
| merged_model.fusion_gates = fusion_gates |
| |
| return merged_model |
| |
| def _neural_synthesis_merge( |
| self, |
| models: List[AutoModelForCausalLM], |
| weights: List[float] |
| ) -> AutoModelForCausalLM: |
| """Neural Synthesis - Creates new parameters by synthesizing across models""" |
| |
| logger.info("Applying neural synthesis merging...") |
| |
| merged_model = models[0] |
| param_keys = list(merged_model.state_dict().keys()) |
| |
| |
| merged_params = {} |
| for key in param_keys: |
| params = [model.state_dict()[key].float() for model in models] |
| stacked = torch.stack(params, dim=0) |
| |
| |
| flat_params = stacked.view(len(models), -1) |
| mean = flat_params.mean(dim=0) |
| |
| |
| deviations = flat_params - mean.unsqueeze(0) |
| |
| |
| synthesized_deviation = torch.zeros_like(mean) |
| for i in range(len(models)): |
| synthesized_deviation += weights[i] * deviations[i] |
| |
| |
| synthesized_param = mean + synthesized_deviation |
| |
| merged_params[key] = synthesized_param.view(stacked.shape[1:]) |
| |
| merged_model.load_state_dict(merged_params) |
| return merged_model |
| |
| def _model_soup_merge( |
| self, |
| models: List[AutoModelForCausalLM], |
| weights: List[float] |
| ) -> AutoModelForCausalLM: |
| """Model Soups - Simple weighted averaging""" |
| |
| logger.info("Applying model soup merging...") |
| |
| merged_model = models[0] |
| param_keys = list(merged_model.state_dict().keys()) |
| |
| merged_params = {} |
| for key in param_keys: |
| weighted_sum = torch.zeros_like(models[0].state_dict()[key], dtype=torch.float32) |
| for i, model in enumerate(models): |
| weighted_sum += weights[i] * model.state_dict()[key].float() |
| merged_params[key] = weighted_sum.to(models[0].state_dict()[key].dtype) |
| |
| merged_model.load_state_dict(merged_params) |
| return merged_model |
| |
| def _analyze_layer_complexity( |
| self, |
| models: List[AutoModelForCausalLM], |
| keys: List[str] |
| ) -> float: |
| """Analyze complexity of a layer""" |
| |
| total_variance = 0.0 |
| count = 0 |
| |
| for key in keys: |
| values = [model.state_dict()[key].float() for model in models] |
| variance = torch.var(torch.stack(values)).item() |
| total_variance += variance |
| count += 1 |
| |
| avg_variance = total_variance / count if count > 0 else 0 |
| |
| |
| complexity = min(1.0, avg_variance / 10.0) |
| |
| return complexity |
|
|
| class ModelMerger: |
| """Main Model Merger Class - Uses HuggingFace for model management""" |
| |
| def __init__(self, config: MergedModelConfig): |
| self.config = config |
| self.model_loader = HuggingFaceModelLoader() |
| self.models = [] |
| self.tokenizers = [] |
| |
| def load_expert(self, expert_config: ExpertConfig): |
| """Load an expert model""" |
| |
| logger.info(f"Loading expert: {expert_config.name}") |
| |
| try: |
| model, tokenizer = self.model_loader.load_model( |
| model_path=expert_config.path, |
| device_map=expert_config.device_map, |
| torch_dtype=expert_config.torch_dtype, |
| load_in_4bit=expert_config.load_in_4bit, |
| load_in_8bit=expert_config.load_in_8bit |
| ) |
| |
| self.models.append(model) |
| self.tokenizers.append(tokenizer) |
| |
| logger.info(f"Successfully loaded: {expert_config.name}") |
| |
| except Exception as e: |
| logger.error(f"Error loading {expert_config.name}: {e}") |
| raise |
| |
| def merge_models(self) -> AutoModelForCausalLM: |
| """Merge all loaded models into a unified model""" |
| |
| if not self.models: |
| raise ValueError("No models loaded!") |
| |
| logger.info(f"Starting merge of {len(self.models)} models...") |
| |
| |
| merger = DeepMerger(self.config.merge_strategy) |
| |
| |
| weights = [expert.weight for expert in self.config.experts] |
| |
| |
| merged_model = merger.merge_models(self.models, weights, self.config) |
| |
| logger.info("Models merged successfully!") |
| |
| return merged_model |
| |
| def save_merged_model( |
| self, |
| model: AutoModelForCausalLM, |
| tokenizer: AutoTokenizer, |
| output_path: str |
| ): |
| """Save the merged model""" |
| |
| logger.info(f"Saving merged model to: {output_path}") |
| |
| os.makedirs(output_path, exist_ok=True) |
| |
| |
| model.save_pretrained(output_path) |
| |
| |
| tokenizer.save_pretrained(output_path) |
| |
| |
| config_path = os.path.join(output_path, "merge_config.json") |
| with open(config_path, 'w') as f: |
| json.dump({ |
| 'merge_strategy': self.config.merge_strategy.value, |
| 'num_experts': len(self.config.experts), |
| 'expert_names': [e.name for e in self.config.experts], |
| 'max_experts_per_token': self.config.max_experts_per_token, |
| 'quality_threshold': self.config.quality_threshold |
| }, f, indent=2) |
| |
| logger.info("Model saved successfully!") |
| |
| def push_to_hub( |
| self, |
| model: AutoModelForCausalLM, |
| tokenizer: AutoTokenizer, |
| model_id: str |
| ): |
| """Push merged model to HuggingFace Hub""" |
| |
| logger.info(f"Pushing model to HuggingFace Hub: {model_id}") |
| |
| try: |
| model.push_to_hub(model_id) |
| tokenizer.push_to_hub(model_id) |
| logger.info("Model pushed successfully!") |
| except Exception as e: |
| logger.error(f"Failed to push model: {e}") |
| raise |
| |
| def cleanup(self): |
| """Cleanup loaded models to free memory""" |
| for path in list(self.model_loader.loaded_models.keys()): |
| self.model_loader.unload_model(path) |
| self.models.clear() |
| self.tokenizers.clear() |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
| def create_merged_model( |
| expert_paths: List[str], |
| expert_names: List[str], |
| output_path: str, |
| merge_strategy: str = "adaptive_fusion", |
| memory_budget: float = 8.0, |
| load_in_4bit: bool = False, |
| push_to_hub: bool = False, |
| hub_model_id: Optional[str] = None |
| ) -> AutoModelForCausalLM: |
| """Convenience function to create a merged model""" |
| |
| |
| experts = [] |
| for name, path in zip(expert_names, expert_paths): |
| experts.append(ExpertConfig( |
| name=name, |
| path=path, |
| weight=1.0 / len(expert_paths), |
| load_in_4bit=load_in_4bit |
| )) |
| |
| |
| config = MergedModelConfig( |
| experts=experts, |
| merge_strategy=MergeStrategy(merge_strategy), |
| max_experts_per_token=4, |
| quality_threshold=0.8, |
| memory_budget=memory_budget, |
| output_path=output_path, |
| push_to_hub=push_to_hub, |
| hub_model_id=hub_model_id |
| ) |
| |
| |
| merger = ModelMerger(config) |
| |
| try: |
| |
| for expert in experts: |
| merger.load_expert(expert) |
| |
| |
| merged_model = merger.merge_models() |
| |
| |
| tokenizer = merger.tokenizers[0] |
| |
| |
| merger.save_merged_model(merged_model, tokenizer, output_path) |
| |
| |
| if push_to_hub and hub_model_id: |
| merger.push_to_hub(merged_model, tokenizer, hub_model_id) |
| |
| return merged_model |
| |
| finally: |
| merger.cleanup() |
|
|
| if __name__ == "__main__": |
| |
| expert_paths = [ |
| "pubertcs/Ornith-1.0-9B-IL2CPP-Decompiler-GGUF", |
| |
| ] |
| |
| expert_names = [ |
| "ornith-il2cpp", |
| |
| ] |
| |
| output_path = "models/merged_model" |
| |
| merged_model = create_merged_model( |
| expert_paths=expert_paths, |
| expert_names=expert_names, |
| output_path=output_path, |
| merge_strategy="adaptive_fusion" |
| ) |
| |
| logger.info(f"Merged model created successfully at: {output_path}") |
|
|