У нас было две бесплатные видеокарты T4 в Kaggle, 30 ГБ оперативной памяти и безумная идея: что будет, если взять веса классической модели (Gemma-4-31B) и хирургическим путем, без всякого дообучения, вшить их в MoE-архитектуру (DeepSeek-V4)?
В академической среде вам скажут, что это невозможно: разные размерности, несовместимые слои нормализации, разные принципы роутинга токенов. Но в парадигме Ghetto MLOps нет слова «невозможно». Есть только вопрос: сколько костылей потребуется, чтобы это скомпилировалось?
Спойлер: нам пришлось взломать реестр Hugging Face, переписать методы инициализации PyTorch в рантайме и написать рекурсивный сонар для поиска спрятанных слоев. В этой статье мы расскажем, как обойти защиты библиотеки transformers и создать собственного ИИ-мутанта.
Анатомия эксперимента
Наша цель состояла в структурном сращивании (Grafting).
-
Донор (Плоть): 4 слоя от 31-миллиардной модели
Gemma(сжатой до 4-бит NF4). -
Экзоскелет: Пустая архитектура
DeepSeek-V4с её хитрым роутером Mixture-of-Experts (MoE).
Звучит просто: загружаем обе модели, циклом for проходимся по слоям, делаем .copy_() нужных матриц и радуемся. На практике библиотека transformers оказала нам ожесточенное сопротивление.
Вот с чем нам пришлось столкнуться и как мы это лечили.
Препятствие 1: Identity Theft и паранойя конфигов
Первое, что сделала библиотека — отказалась признавать наш кастомный тип модели gemma4. Мы обошли это, принудительно зарегистрировав тип в глобальном словаре CONFIG_MAPPING.
Но дальше началась мистика. При попытке загрузить модель, transformers выдавал ошибку: AttributeError: 'dict' object has no attribute 'to_dict'.
Оказалось, внутри модуля generation библиотека случайно десериализует объект конфигурации в обычный питоновский словарь (dict), а затем сама же падает, пытаясь вызвать у него свои внутренние методы.
Решение (Monkey-patching): Мы написали «бронебойный патч». Если функция падает с AttributeError, мы на лету заворачиваем словарь в кастомный Proxy-класс, который притворяется объектом конфигурации.
Препятствие 2: Квантовый парадокс инициализации
Поскольку экзоскелет DeepSeek физически больше 4 слоев Геммы, библиотека решила заполнить недостающие пустоты случайным шумом, вызвав метод normal_ (нормальное распределение).
И тут PyTorch впал в кому: NotImplementedError: "normal_kernel_cuda" not implemented for 'Byte'.
Дело в том, что веса донора загружались через bitsandbytes в 4-битах (как сырые байты uint8). А генератор шума PyTorch работает только с числами с плавающей точкой (float).
Решение: Мы перехватили вызов TORCH_INIT_FUNCTIONS["normal_"] прямо в исходниках библиотеки и запретили ей трогать тензоры, если они сжаты в байты.
Препятствие 3: Спрятанные эксперты и OOM
Архитекторы DeepSeek оказались затейниками: они не положили MoE-экспертов в обычный ModuleList, а завернули их в монолитный класс DeepseekV2Experts. Питон отказался по нему итерироваться. Более того, при попытке распаковать 31B-слой Геммы для переноса весов, мы мгновенно ловили Out Of Memory (OOM) в оперативной памяти Kaggle.
Решение: 1. Мы написали «умный сонар» — рекурсивную функцию, которая ныряет в любую структуру классов и ищет слои по наличию атрибутов gate_proj и up_proj.
2. Для борьбы с OOM мы разнесли модели по разным GPU, а веса переносили микро-порциями через CPU, мгновенно вызывая gc.collect(), чтобы сборщик мусора очищал оперативку.
Идеальный скрипт некроманта
После десятков падений мы выковали монолитный код, который обходит все защиты и производит успешную трансплантацию. Этот скрипт — готовый шаблон для скрещивания любых несовместимых моделей.
Python
import torchimport osimport gcimport transformers.generation.configuration_utils as gen_utilsfrom transformers import ( AutoConfig, AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, GemmaConfig, CONFIG_MAPPING, GenerationConfig)from transformers.initialization import TORCH_INIT_FUNCTIONS# --- 1. ОЧИСТКА ПАМЯТИ ---def cleanup(): gc.collect() torch.cuda.empty_cache()cleanup()# --- 2. ЯДЕРНЫЕ ПАТЧИ (Обход защит библиотеки) ---print("🛠 Запуск патчей совместимости...")# Патч 2.1: Исправление бага с dict.to_dict()original_from_model_config = GenerationConfig.from_model_config@classmethoddef patched_from_model_config(cls, model_config): try: return original_from_model_config(model_config) except AttributeError: class ForcedConfig: def __init__(self, d): self.d = d if isinstance(d, dict) else {} def to_dict(self): return self.d def get_text_config(self, *args, **kwargs): return self def __getattr__(self, name): return self.d.get(name, None) return original_from_model_config(ForcedConfig(model_config))gen_utils.GenerationConfig.from_model_config = patched_from_model_config# Патч 2.2: Блокировка нормального распределения для 4-bit весовoriginal_normal = TORCH_INIT_FUNCTIONS["normal_"]def safe_normal_(tensor, mean=0.0, std=1.0, generator=None): if tensor.dtype in [torch.uint8, torch.int8]: return tensor return original_normal(tensor, mean=mean, std=std, generator=generator)TORCH_INIT_FUNCTIONS["normal_"] = safe_normal_# --- 3. НАСТРОЙКИ И РЕГИСТРАЦИЯ ---SKELETON_ID = "livadies/DeepSeek-V4-Pro-Ghetto-MoE-2-Experts"DONOR_ID = "livadies/gemma-4-31B-Base-Ghetto-NF4"CONFIG_MAPPING.register("gemma4", GemmaConfig, exist_ok=True)# --- 4. ЗАГРУЗКА ДОНОРА (GPU 1) ---print("📡 Загружаем донора на GPU 1...")bnb_config = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4")donor = AutoModelForCausalLM.from_pretrained( DONOR_ID, quantization_config=bnb_config, device_map={"": 1}, low_cpu_mem_usage=True, trust_remote_code=True)# --- 5. СКЕЛЕТ (CPU) ---print("📡 Создаем пустой экзоскелет на CPU...")config_v4 = AutoConfig.from_pretrained(SKELETON_ID, trust_remote_code=True)with torch.device("cpu"): model_v4 = AutoModelForCausalLM.from_config(config_v4, trust_remote_code=True).half()# --- 6. ХИРУРГИЯ (Умная трансплантация с экономией RAM) ---def adapt_weight(donor_w, target_shape): """Безопасная подгонка размеров с заполнением нулями""" with torch.no_grad(): d_tensor = donor_w.to(device="cpu", dtype=torch.float16) new_w = torch.zeros(target_shape, dtype=torch.float16, device="cpu") h, w = min(d_tensor.shape[0], target_shape[0]), min(d_tensor.shape[1], target_shape[1]) new_w[:h, :w] = d_tensor[:h, :w].clone() return new_wdef find_experts(node): """Рекурсивный сонар для обхода кастомных классов вроде DeepseekV2Experts""" found = [] if hasattr(node, 'gate_proj') and hasattr(node, 'up_proj') and hasattr(node, 'down_proj'): found.append(node) elif isinstance(node, (torch.nn.ModuleList, list, tuple)): for child in node: found.extend(find_experts(child)) elif hasattr(node, 'children'): for child in node.children(): found.extend(find_experts(child)) return foundprint("💉 Начинаем пересадку весов...")d_model = donor.model if hasattr(donor, 'model') else donord_layers = getattr(d_model, 'layers', getattr(d_model, 'h', getattr(d_model, 'blocks', None)))with torch.no_grad(): for i in range(4): # Пересаживаем 4 слоя print(f"🧬 Слой {i}: Сшиваем компоненты...") dl, vl = d_layers[i], model_v4.model.layers[i] d_mlp = getattr(dl, 'mlp', getattr(dl, 'ffn', None)) # Пересадка MLP-экспертов if d_mlp: experts = find_experts(vl.mlp) for expert in experts: expert.gate_proj.weight.copy_(adapt_weight(d_mlp.gate_proj.weight, expert.gate_proj.weight.shape)) expert.up_proj.weight.copy_(adapt_weight(d_mlp.up_proj.weight, expert.up_proj.weight.shape)) expert.down_proj.weight.copy_(adapt_weight(d_mlp.down_proj.weight, expert.down_proj.weight.shape)) cleanup() # Очистка после каждой матрицы! # Пересадка Attention vl.self_attn.q_b_proj.weight.copy_(adapt_weight(dl.self_attn.q_proj.weight, vl.self_attn.q_b_proj.weight.shape)) vl.self_attn.o_proj.weight.copy_(adapt_weight(dl.self_attn.o_proj.weight, vl.self_attn.o_proj.weight.shape)) cleanup()# --- 7. УТИЛИЗАЦИЯ И ЗАПУСК ---print("🗑 Сжигаем донора для освобождения VRAM...")del donorcleanup()print("🚀 Оживляем Химеру на GPU 0...")model_v4 = model_v4.to("cuda:0")# Патч роутера для обхода конфликта размерностей при инференсеdef ghetto_route_final(self, logits): w = torch.nn.functional.softmax(logits.view(-1, logits.shape[-1]) + 1e-6, dim=-1) tw, ti = torch.topk(w, k=self.top_k, dim=-1) return ti, tw * self.routed_scaling_factorfor layer in model_v4.model.layers: if hasattr(layer, 'mlp') and hasattr(layer.mlp, 'route_tokens_to_experts'): layer.mlp.route_tokens_to_experts = ghetto_route_final.__get__(layer.mlp)print("✨ Проверка сознания Химеры...")tok = AutoTokenizer.from_pretrained("deepseek-ai/DeepSeek-V2", trust_remote_code=True)inputs = tok("The experimental hybrid AI said:", return_tensors="pt").to("cuda:0")with torch.no_grad(): outputs = model_v4.generate(**inputs, max_new_tokens=40, do_sample=True, temperature=0.85)print("\n" + "="*40 + f"\n📟 ОТВЕТ:\n{tok.decode(outputs[0], skip_special_tokens=True)}\n" + "="*40)
Что в итоге?
Конечно, без Fine-Tuning’а, согласования словарей (Vocab Size) и проекций скрытых состояний, модель генерирует чистую «цифровую шизофрению»:
TheexperimentalhybridAIsaid:vdotsD...Buddhist...tomatosupervised...
Но суть этого эксперимента не в том, чтобы получить ChatGPT за ноль рублей. Суть в доказательстве концепции: в машинном обучении нет непробиваемых стен архитектуры. Имея базовое понимание тензоров, Python и горсть костылей, вы можете скрестить ужа с ежом прямо в бесплатном ноутбуке Kaggle.
Репозиторий-мавзолей с нашей Химерой доступен на Hugging Face: livadies/DeepGemma-V4-Chimera-Ghetto-MoE
Добро пожаловать в Ghetto MLOps. Ломайте библиотеки с удовольствием!
ссылка на оригинал статьи https://habr.com/ru/articles/1028910/