add merge tools

This commit is contained in:
Serge Zaigraeff
2025-03-04 22:10:20 +03:00
parent 1af3ebd80e
commit fcd3b936cf
3 changed files with 657 additions and 2 deletions
+367
View File
@@ -0,0 +1,367 @@
#!/bin/bash
# Имя окружения conda
CONDA_ENV_NAME="whisper-merge"
# Директории для моделей
MODEL_A="/mnt/cloud/llm/whisper/whisper-large-v3-russian"
MODEL_B="/mnt/cloud/llm/whisper/whisper-large-v3-ru-podlodka"
OUTPUT_DIR="/mnt/cloud/llm/whisper/whisper-large-v3-russian+podlodka"
# Флаг обновления (по умолчанию false)
UPDATE_ENV=false
# Метод слияния (по умолчанию all)
MERGE_METHOD="all"
# Дополнительные параметры (по умолчанию)
SLERP_T=0.5
TIES_DENSITY=0.8
ENCODER_WEIGHTS="0.6,0.4"
DECODER_WEIGHTS="0.4,0.6"
DIRECT_ALPHA=0.5
# Проверка аргументов
while [[ "$#" -gt 0 ]]; do
case $1 in
--update) UPDATE_ENV=true ;;
--method) MERGE_METHOD="$2"; shift ;;
--slerp-t) SLERP_T="$2"; shift ;;
--ties-density) TIES_DENSITY="$2"; shift ;;
--encoder-weights) ENCODER_WEIGHTS="$2"; shift ;;
--decoder-weights) DECODER_WEIGHTS="$2"; shift ;;
--direct-alpha) DIRECT_ALPHA="$2"; shift ;;
*) echo "⚠️ Неизвестный параметр: $1"; exit 1 ;;
esac
shift
done
# Проверка наличия conda
if ! command -v conda &> /dev/null; then
echo "❌ Conda не установлен. Пожалуйста, установите conda и попробуйте снова."
exit 1
fi
# Создание окружения conda, если оно не существует
if ! conda env list | grep -q "$CONDA_ENV_NAME"; then
echo "🔹 Создание окружения conda: $CONDA_ENV_NAME"
conda create -n "$CONDA_ENV_NAME" python=3.12 -y
else
echo "✅ Окружение conda '$CONDA_ENV_NAME' уже существует."
fi
# Получение пути к conda
CONDA_PATH=$(which conda)
# Проверка, что путь к conda найден
if [ -z "$CONDA_PATH" ]; then
echo "❌ Не удалось найти путь к conda."
exit 1
fi
# Активация окружения conda
echo "🔹 Активация окружения conda: $CONDA_ENV_NAME"
source $(dirname "$CONDA_PATH")/../etc/profile.d/conda.sh
conda activate "$CONDA_ENV_NAME"
# Если флаг --update установлен, обновляем зависимости
if [[ "$UPDATE_ENV" == true ]]; then
echo "🔹 Установка и обновление зависимостей"
pip install --upgrade pip wheel
pip install torch torchaudio
pip install transformers datasets evaluate accelerate
pip install soundfile librosa tqdm
fi
# Проверка установки torch и transformers
if ! python -c "import torch, transformers" &> /dev/null; then
echo "❌ PyTorch или Transformers не установлены. Запустите с опцией --update."
exit 1
fi
# Создание временного Python-скрипта для слияния
cat > merge_models.py << EOL
#!/usr/bin/env python3
import argparse
import os
import torch
from transformers import WhisperForConditionalGeneration, WhisperProcessor
from tqdm import tqdm
def direct_merge(model_a_path, model_b_path, output_path, alpha=0.5):
"""Простое прямое слияние весов моделей с коэффициентом alpha"""
print(f"🔹 Запуск прямого слияния с alpha={alpha}")
print(f"📂 Загрузка модели A из {model_a_path}")
model_a = WhisperForConditionalGeneration.from_pretrained(model_a_path)
print(f"📂 Загрузка модели B из {model_b_path}")
model_b = WhisperForConditionalGeneration.from_pretrained(model_b_path)
# Создаем новую модель для хранения объединенных весов
print("🔄 Объединение весов моделей...")
merged_model = WhisperForConditionalGeneration.from_pretrained(model_a_path)
# Подсчет общего количества параметров
param_count = len(list(merged_model.named_parameters()))
# Объединение весов
with torch.no_grad():
for i, (param_name, param) in enumerate(tqdm(merged_model.named_parameters(), total=param_count, desc="Слияние параметров")):
if param_name in model_a.state_dict() and param_name in model_b.state_dict():
# Объединяем веса с учетом коэффициента альфа
merged_param = alpha * model_a.state_dict()[param_name] + (1 - alpha) * model_b.state_dict()[param_name]
param.copy_(merged_param)
# Создаем директорию для выходных данных, если она не существует
os.makedirs(output_path, exist_ok=True)
# Сохраняем объединенную модель
print(f"💾 Сохранение объединенной модели в {output_path}")
merged_model.save_pretrained(output_path)
# Сохраняем процессор из модели A
print("💾 Сохранение процессора")
processor = WhisperProcessor.from_pretrained(model_a_path)
processor.save_pretrained(output_path)
print("✅ Прямое слияние успешно завершено!")
return merged_model
def slerp_weights(model_a, model_b, t):
"""Выполняет SLERP (Spherical Linear Interpolation) между весами моделей"""
# Получаем словари состояний моделей
state_dict_a = model_a.state_dict()
state_dict_b = model_b.state_dict()
# Создаем новый словарь для смешанных весов
mixed_weights = {}
for key in tqdm(state_dict_a.keys(), desc="SLERP слияние"):
if key in state_dict_b:
# Получаем веса для текущего параметра
weight_a = state_dict_a[key].float()
weight_b = state_dict_b[key].float()
# Выполняем SLERP
# Нормализуем веса
norm_a = torch.norm(weight_a)
norm_b = torch.norm(weight_b)
# Если норма близка к нулю, используем линейную интерполяцию
if norm_a < 1e-6 or norm_b < 1e-6:
mixed_weights[key] = (1 - t) * weight_a + t * weight_b
continue
weight_a_normalized = weight_a / norm_a
weight_b_normalized = weight_b / norm_b
# Вычисляем косинус угла между векторами
cos_theta = torch.sum(weight_a_normalized * weight_b_normalized)
cos_theta = torch.clamp(cos_theta, -1.0, 1.0)
# Вычисляем угол
theta = torch.acos(cos_theta)
# Если угол близок к 0 или π, используем линейную интерполяцию
if abs(theta) < 1e-6 or abs(theta - torch.pi) < 1e-6:
mixed_weights[key] = (1 - t) * weight_a + t * weight_b
else:
# SLERP формула: (sin((1-t)*theta) / sin(theta)) * weight_a + (sin(t*theta) / sin(theta)) * weight_b
sin_theta = torch.sin(theta)
mixed_weights[key] = (torch.sin((1 - t) * theta) / sin_theta) * weight_a + (torch.sin(t * theta) / sin_theta) * weight_b
# Проверка на NaN
if torch.isnan(mixed_weights[key]).any():
# Если появились NaN, используем линейную интерполяцию
mixed_weights[key] = (1 - t) * weight_a + t * weight_b
return mixed_weights
def slerp_merge(model_a_path, model_b_path, output_path, t=0.5):
"""Слияние методом SLERP, реализованное напрямую без использования mergekit"""
print(f"🔹 Запуск слияния методом SLERP с t={t}")
print(f"📂 Загрузка модели A из {model_a_path}")
model_a = WhisperForConditionalGeneration.from_pretrained(model_a_path)
print(f"📂 Загрузка модели B из {model_b_path}")
model_b = WhisperForConditionalGeneration.from_pretrained(model_b_path)
# Создаем новую модель для хранения смешанных весов
print("🔄 Применение метода SLERP...")
merged_model = WhisperForConditionalGeneration.from_pretrained(model_a_path)
# Получаем смешанные веса
mixed_weights = slerp_weights(model_a, model_b, t)
# Загружаем смешанные веса в модель
merged_model.load_state_dict(mixed_weights)
# Создаем директорию для выходных данных
os.makedirs(output_path, exist_ok=True)
# Сохраняем объединенную модель
print(f"💾 Сохранение объединенной модели в {output_path}")
merged_model.save_pretrained(output_path)
# Сохраняем процессор из модели A
processor = WhisperProcessor.from_pretrained(model_a_path)
processor.save_pretrained(output_path)
print("✅ Слияние методом SLERP успешно завершено!")
return merged_model
def ties_merge(model_a_path, model_b_path, output_path, density=0.8, encoder_weights=(0.6, 0.4), decoder_weights=(0.4, 0.6)):
"""Слияние, реализованное по принципу TIES без использования mergekit"""
print(f"🔹 Запуск слияния по принципу TIES с плотностью {density}")
print(f" Веса кодировщика: {encoder_weights}, веса декодера: {decoder_weights}")
print(f"📂 Загрузка модели A из {model_a_path}")
model_a = WhisperForConditionalGeneration.from_pretrained(model_a_path)
print(f"📂 Загрузка модели B из {model_b_path}")
model_b = WhisperForConditionalGeneration.from_pretrained(model_b_path)
# Создаем новую модель для хранения смешанных весов
print("🔄 Применение метода TIES...")
merged_model = WhisperForConditionalGeneration.from_pretrained(model_a_path)
# Получаем словари состояний моделей
state_dict_a = model_a.state_dict()
state_dict_b = model_b.state_dict()
# Создаем словарь для смешанных весов
mixed_weights = {}
# Для каждого параметра
for key in tqdm(state_dict_a.keys(), desc="TIES слияние"):
if key in state_dict_b:
# Определение типа слоя (кодировщик или декодер)
if "encoder" in key:
w_a, w_b = encoder_weights
elif "decoder" in key:
w_a, w_b = decoder_weights
else:
# Для других параметров используем среднее
w_a = (encoder_weights[0] + decoder_weights[0]) / 2
w_b = (encoder_weights[1] + decoder_weights[1]) / 2
# Получаем веса для текущего параметра
weight_a = state_dict_a[key]
weight_b = state_dict_b[key]
# Вычисляем абсолютную разницу между весами
abs_diff = torch.abs(weight_a - weight_b)
# Создаем маску для топ-k% элементов с наибольшей разницей
# если density=0.8, то мы сохраняем 80% элементов с наибольшей разницей
k = int((1 - density) * abs_diff.numel())
if k > 0:
threshold = torch.kthvalue(abs_diff.flatten(), k).values
mask = abs_diff >= threshold
else:
mask = torch.ones_like(abs_diff, dtype=torch.bool)
# Создаем смешанные веса:
# Для элементов с большой разницей используем веса w_a и w_b
# Для остальных используем среднее арифметическое
mixed = torch.zeros_like(weight_a)
mixed[mask] = w_a * weight_a[mask] + w_b * weight_b[mask]
mixed[~mask] = (weight_a[~mask] + weight_b[~mask]) / 2
mixed_weights[key] = mixed
# Загружаем смешанные веса в модель
merged_model.load_state_dict(mixed_weights)
# Создаем директорию для выходных данных
os.makedirs(output_path, exist_ok=True)
# Сохраняем объединенную модель
print(f"💾 Сохранение объединенной модели в {output_path}")
merged_model.save_pretrained(output_path)
# Сохраняем процессор из модели A
processor = WhisperProcessor.from_pretrained(model_a_path)
processor.save_pretrained(output_path)
print("✅ Слияние по принципу TIES успешно завершено!")
return merged_model
def main():
parser = argparse.ArgumentParser(description="Объединение моделей Whisper методами SLERP и TIES")
parser.add_argument("--model-a", default="$MODEL_A",
help="Путь к первой модели")
parser.add_argument("--model-b", default="$MODEL_B",
help="Путь к второй модели")
parser.add_argument("--output-dir", default="$OUTPUT_DIR",
help="Директория для сохранения результатов")
parser.add_argument("--method", choices=["slerp", "ties", "direct", "all"], default="$MERGE_METHOD",
help="Метод слияния: slerp, ties, direct или all")
# Параметры для SLERP
parser.add_argument("--slerp-t", type=float, default=$SLERP_T,
help="Коэффициент интерполяции для SLERP (0-1)")
# Параметры для TIES
parser.add_argument("--ties-density", type=float, default=$TIES_DENSITY,
help="Плотность смешивания для TIES (0-1)")
parser.add_argument("--encoder-weights", type=str, default="$ENCODER_WEIGHTS",
help="Веса для кодировщика в формате '0.6,0.4'")
parser.add_argument("--decoder-weights", type=str, default="$DECODER_WEIGHTS",
help="Веса для декодера в формате '0.4,0.6'")
# Параметры для прямого слияния
parser.add_argument("--direct-alpha", type=float, default=$DIRECT_ALPHA,
help="Коэффициент альфа для прямого слияния (0-1)")
args = parser.parse_args()
# Разбор весов для TIES
encoder_weights = tuple(map(float, args.encoder_weights.split(',')))
decoder_weights = tuple(map(float, args.decoder_weights.split(',')))
# Создание выходной директории
os.makedirs(args.output_dir, exist_ok=True)
# Выполнение слияния выбранным методом
if args.method == "direct" or args.method == "all":
direct_output = os.path.join(args.output_dir, "direct")
direct_merge(args.model_a, args.model_b, direct_output, args.direct_alpha)
if args.method == "slerp" or args.method == "all":
slerp_output = os.path.join(args.output_dir, "slerp")
try:
slerp_merge(args.model_a, args.model_b, slerp_output, args.slerp_t)
except Exception as e:
print(f"❌ Ошибка при слиянии методом SLERP: {e}")
print("⚠️ Используется прямое слияние вместо SLERP.")
direct_merge(args.model_a, args.model_b, slerp_output, args.slerp_t)
if args.method == "ties" or args.method == "all":
ties_output = os.path.join(args.output_dir, "ties")
try:
ties_merge(args.model_a, args.model_b, ties_output, args.ties_density, encoder_weights, decoder_weights)
except Exception as e:
print(f"❌ Ошибка при слиянии методом TIES: {e}")
print("⚠️ Используется прямое слияние вместо TIES.")
alpha = (encoder_weights[0] + decoder_weights[0]) / 2
direct_merge(args.model_a, args.model_b, ties_output, alpha)
print("\n✅ Все операции слияния успешно завершены!")
print(f"📂 Результаты сохранены в {args.output_dir}")
if __name__ == "__main__":
main()
EOL
# Запуск Python-скрипта
python merge_models.py
# Удаление временного скрипта
rm merge_models.py
echo ""
echo "✅ Процесс слияния завершен. Результаты сохранены в $OUTPUT_DIR"