Почему ваш GPU плачет, когда вы пытаетесь дообучить 30B модель
Вы скачали свежую Qwen3-32B-Instruct, запустили скрипт тонкой настройки и... получили OutOfMemory на первом же батче. Знакомо? Ваш RTX 4090 с 24 ГБ VRAM внезапно кажется картошкой. А что говорить о тех, у кого RTX 4070 Ti Super с 16 ГБ или, не дай бог, RTX 4060 с 8 ГБ?
Проблема в том, что даже квантованная модель в формате GGUF требует полной разквантовки для обучения. Веса из 4 бит превращаются обратно в 16 бит, и память GPU улетает в ноль. Традиционные методы вроде LoRA или QLoRA помогают, но не достаточно.
Ключевой момент: QLoRA экономит память только на хранении весов адаптеров. Основная модель все равно должна быть загружена в VRAM в полном размере (хоть и в 4-битном формате). Для 32B модели это примерно 16-20 ГБ VRAM. Не каждый GPU столько имеет.
Спасательный круг: оффлоадинг в RAM через Unsloth
Unsloth в версии 2026 года (актуально на 08.02.2026) научился делать то, о чем другие библиотеки только мечтают: он может держать квантованные веса в оперативной памяти, а на GPU загружать только то, что нужно прямо сейчас для forward/backward pass.
Представьте: у вас 32 ГБ RAM и всего 8 ГБ VRAM. Unsloth загружает 4-битную модель в RAM (занимает ~16 ГБ), а на GPU держит только активные слои и градиенты. Когда нужно обработать следующий слой - он подгружает его из RAM, выгружает предыдущий. Медленнее? Да. Но работает там, где другие методы просто отказываются запускаться.
1 Подготовка: что нужно перед началом
Первое - проверьте ваше железо. Техника работает если:
- У вас минимум 32 ГБ оперативной памяти (лучше 64 ГБ)
- GPU с 8+ ГБ VRAM (RTX 4060, RTX 4070, даже некоторые мобильные карты)
- PCIe 4.0 или лучше (скорость обмена с RAM критична)
- Python 3.10+ и свежий PyTorch 2.4+
2 Установка Unsloth с поддержкой оффлоадинга
Не устанавливайте Unsloth через pip install unsloth. Это даст вам базовую версию без оффлоадинга. Нужна специальная сборка:
# Удаляем старый unsloth если был
pip uninstall unsloth -y
# Ставим версию с оффлоадингом
pip install "unsloth[offload] @ git+https://github.com/unslothai/unsloth.git"
# Обязательные зависимости
pip install torch==2.4.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install transformers==4.45.0 accelerate==0.30.0 peft==0.11.0 trl==0.9.0
pip install bitsandbytes==0.43.0 # для 4-битного квантования
Почему именно эти версии? Потому что на 08.02.2026 это самые стабильные комбинации. Библиотеки обновляются каждую неделю, и если взять что-то новее - можете получить несовместимость.
3 Загрузка и подготовка модели с Q4 квантованием
Вот как НЕ надо делать:
# ПЛОХО: так вы загрузите модель полностью в VRAM
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen3-32B-Instruct",
torch_dtype=torch.float16,
device_map="auto"
)
Ваш GPU умрет. Вместо этого используем Unsloth с оффлоадингом:
import torch
from unsloth import FastLanguageModel
from transformers import TrainingArguments
# Ключевой параметр: offload_folder
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="Qwen/Qwen3-32B-Instruct",
max_seq_length=2048, # не ставьте больше, чем нужно
dtype=torch.float16,
load_in_4bit=True, # 4-битное квантование
offload_folder="./offload", # папка для оффлоадинга в RAM
offload_enabled=True, # включаем оффлоадинг
device_map="auto",
use_gradient_checkpointing=True, # экономит память на градиентах
)
# Конвертируем в PEFT модель для LoRA
model = FastLanguageModel.get_peft_model(
model,
r=16, # rank адаптеров
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing=True,
random_state=3407,
max_seq_length=2048,
)
Внимание: offload_folder должен быть на быстром SSD, а не на HDD. Иначе скорость обмена упадет в 10-100 раз. Если у вас только HDD - лучше использовать RAM-диск (tmpfs в Linux).
4 Настройка тренировочных параметров под слабое железо
Здесь большинство совершает ошибку: берут параметры из туториалов для A100 и пытаются запустить на RTX 4060. Не работает.
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=3,
per_device_train_batch_size=1, # ДА, всего 1! Не пытайтесь поставить 2 или 4
per_device_eval_batch_size=1,
gradient_accumulation_steps=8, # Компенсируем маленький batch size
warmup_steps=50,
logging_steps=10,
save_steps=500,
eval_steps=500,
save_total_limit=2,
learning_rate=2e-4,
fp16=True, # Используем mixed precision
gradient_checkpointing=True,
optim="paged_adamw_8bit", # 8-битный оптимизатор экономит память
lr_scheduler_type="cosine",
report_to="none", # Отключаем wandb/tensorboard чтобы не тратить память
ddp_find_unused_parameters=False,
remove_unused_columns=False,
max_grad_norm=0.3,
# Критически важные параметры для оффлоадинга:
offload_params_to_cpu=False, # Unsloth сам управляет оффлоадингом
offload_activations=False,
# Управление памятью:
torch_compile=False, # Отключаем! Съедает много памяти
dataloader_pin_memory=False, # Не pin-ить данные в GPU памяти
dataloader_num_workers=0, # 0 для Windows, 2-4 для Linux
)
Почему per_device_train_batch_size=1? Потому что каждый пример в батче требует хранения активаций для всех слоев. С batch_size=2 вам нужно в 2 раза больше памяти для активаций. А активации - это главный пожиратель VRAM после весов модели.
5 Подготовка датасета и запуск обучения
Датасет должен быть подготовлен правильно. Нельзя просто взять JSONL и скормить модели:
from datasets import Dataset
import pandas as pd
# Пример правильного формата для инструктивного датасета
data = [
{
"instruction": "Напиши email клиенту об отсрочке платежа",
"input": "Клиент Иван Петров, сумма 50000 руб, отсрочка 14 дней",
"output": "Уважаемый Иван Петров..."
},
# ... больше примеров
]
dataset = Dataset.from_pandas(pd.DataFrame(data))
# Токенизация с учетом формата Qwen3
def tokenize_function(examples):
texts = []
for i in range(len(examples["instruction"])):
# Формат для Qwen3
message = [
{"role": "user", "content": examples["instruction"][i] + "\n" + examples["input"][i]},
{"role": "assistant", "content": examples["output"][i]}
]
text = tokenizer.apply_chat_template(
message,
tokenize=False,
add_generation_prompt=False
)
texts.append(text)
tokenized = tokenizer(
texts,
truncation=True,
padding="max_length",
max_length=2048,
)
# Для causal LM метки такие же как input_ids
tokenized["labels"] = tokenized["input_ids"].copy()
return tokenized
tokenized_dataset = dataset.map(tokenize_function, batched=True)
Теперь запускаем обучение:
from trl import SFTTrainer
from unsloth import is_bfloat16_supported
# Проверяем поддержку bfloat16 (нужно для некоторых карт)
if is_bfloat16_supported():
training_args.bf16 = True
training_args.fp16 = False
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=tokenized_dataset,
dataset_text_field="text", # поле с текстом
max_seq_length=2048,
packing=False, # Отключаем packing - он требует больше памяти
)
# Запускаем!
trainer.train()
Что делать когда все равно не хватает памяти
Ситуация: вы все сделали по инструкции, но получили CUDA out of memory. Такое бывает. Вот что проверять:
- Уменьшите max_seq_length. 2048 - это много. Попробуйте 1024 или 512. Каждые 512 токенов - это примерно 1 ГБ дополнительной памяти для активаций.
- Увеличьте gradient_accumulation_steps. Если было 8, поставьте 16. Это позволит уменьшить per_device_train_batch_size до 1 (да, можно меньше 1 не ставить).
- Отключите gradient_checkpointing в модели. Звучит парадоксально, но иногда он сам ест память. Удалите use_gradient_checkpointing=True из get_peft_model.
- Используйте более агрессивное квантование. Вместо load_in_4bit=True попробуйте load_in_3bit=True. Но качество модели упадет.
Если ничего не помогает - ваша модель слишком велика для железа. Придется выбирать меньшую. Например, вместо Qwen3-32B взять Qwen3-14B или даже 7B.
Мониторинг использования памяти в реальном времени
Не гадайте, сколько памяти используется. Смотрите:
import torch
def print_memory_usage():
print(f"VRAM used: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
print(f"VRAM reserved: {torch.cuda.memory_reserved() / 1e9:.2f} GB")
print(f"VRAM max allocated: {torch.cuda.max_memory_allocated() / 1e9:.2f} GB")
if hasattr(torch.cuda, 'memory_stats'):
stats = torch.cuda.memory_stats()
print(f"Active allocations: {stats.get('num_alloc_retries', 0)}")
# Вызывайте эту функцию в коллбэке trainer
from transformers import TrainerCallback
class MemoryCallback(TrainerCallback):
def on_log(self, args, state, control, logs=None, **kwargs):
print_memory_usage()
Добавьте callback в trainer:
trainer = SFTTrainer(
# ... остальные параметры
callbacks=[MemoryCallback()],
)
Сохранение и загрузка дообученной модели
После обучения нужно сохранить не только адаптеры LoRA, но и информацию о квантовании:
# Сохраняем модель
model.save_pretrained("./my_finetuned_model")
tokenizer.save_pretrained("./my_finetuned_model")
# Сохраняем конфигурацию квантования
import json
with open("./my_finetuned_model/quantization_config.json", "w") as f:
json.dump({
"bits": 4,
"offload_enabled": True,
"offload_folder": "./offload",
}, f)
Для загрузки дообученной модели:
# Загружаем с теми же параметрами оффлоадинга
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="./my_finetuned_model",
max_seq_length=2048,
dtype=torch.float16,
load_in_4bit=True,
offload_folder="./offload",
offload_enabled=True,
device_map="auto",
)
Почему это работает медленнее (и как ускорить)
Оффлоадинг в RAM добавляет overhead. Каждый слой нужно подгружать из RAM в VRAM, потом выгружать обратно. На PCIe 4.0 x16 это добавляет примерно 30-50% к времени эпохи.
Как ускорить:
- Используйте NVMe SSD как offload_folder. Скорость чтения/записи 7000 MB/s против 300 MB/s у SATA SSD.
- Увеличьте batch size если позволяет память. Но осторожно - каждый +1 к batch size требует много памяти.
- Отключите сбор метрик в реальном времени. Каждый расчет accuracy/loss требует дополнительных проходов.
- Используйте более простую модель архитектуры. Mistral обрабатывается быстрее чем Qwen при том же количестве параметров.
Чего ждать от Unsloth в будущем
На 08.02.2026 Unsloth уже поддерживает оффлоадинг, но разработчики обещают в ближайших релизах:
- Автоматическую оптимизацию offload_folder (сам выберет RAM-диск если доступно)
- Поддержку оффлоадинга на несколько GPU (сейчас только single GPU)
- Интеграцию с техниками распределения моделей на несколько карт
- Более умное кэширование слоев (часто используемые слои остаются в VRAM)
Пока этого нет - используйте текущую реализацию. Она грубая, но работает. Как молоток: не самый изящный инструмент, но гвозди забивает.
Последний совет: не пытайтесь дообучить 70B модель на RTX 4060. Даже с оффлоадингом. Некоторые вещи требуют правильного железа. Но 32B на 8 ГБ VRAM - теперь реальность. Пусть и медленная.