У нас было две бесплатные видеокарты 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 torch
import os
import gc
import transformers.generation.configuration_utils as gen_utils
from 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
@classmethod
def 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_w
def 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 found
print("💉 Начинаем пересадку весов...")
d_model = donor.model if hasattr(donor, 'model') else donor
d_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 donor
cleanup()
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_factor
for 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. Ломайте библиотеки с удовольствием!
Автор: Livadies


