Unsloth Q4 квантование: тонкая настройка LLM на слабом GPU с оффлоадингом | AiManual
AiManual Logo Ai / Manual.
08 Фев 2026 Гайд

Unsloth + Q4 квантование: как заставить 30B модель работать на 8 ГБ VRAM с оффлоадингом в RAM

Пошаговая инструкция по тонкой настройке больших языковых моделей с Unsloth, Q4 квантованием и оффлоадингом в RAM. Работаем с 30B моделями на 8 ГБ VRAM.

Почему ваш 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+
💡
Если у вас AMD GPU без CUDA - забудьте. Unsloth работает только с NVIDIA через CUDA. Для AMD есть другие решения, но они не поддерживают оффлоадинг так же эффективно.

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. Такое бывает. Вот что проверять:

  1. Уменьшите max_seq_length. 2048 - это много. Попробуйте 1024 или 512. Каждые 512 токенов - это примерно 1 ГБ дополнительной памяти для активаций.
  2. Увеличьте gradient_accumulation_steps. Если было 8, поставьте 16. Это позволит уменьшить per_device_train_batch_size до 1 (да, можно меньше 1 не ставить).
  3. Отключите gradient_checkpointing в модели. Звучит парадоксально, но иногда он сам ест память. Удалите use_gradient_checkpointing=True из get_peft_model.
  4. Используйте более агрессивное квантование. Вместо 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% к времени эпохи.

Как ускорить:

  1. Используйте NVMe SSD как offload_folder. Скорость чтения/записи 7000 MB/s против 300 MB/s у SATA SSD.
  2. Увеличьте batch size если позволяет память. Но осторожно - каждый +1 к batch size требует много памяти.
  3. Отключите сбор метрик в реальном времени. Каждый расчет accuracy/loss требует дополнительных проходов.
  4. Используйте более простую модель архитектуры. Mistral обрабатывается быстрее чем Qwen при том же количестве параметров.
💡
Если скорость критична - рассмотрите аренду GPU в облаке. Но помните: обучение 32B модели даже на A100 займет несколько часов и будет стоить $20-50. На своем железе с оффлоадингом - сутки, но бесплатно.

Чего ждать от Unsloth в будущем

На 08.02.2026 Unsloth уже поддерживает оффлоадинг, но разработчики обещают в ближайших релизах:

  • Автоматическую оптимизацию offload_folder (сам выберет RAM-диск если доступно)
  • Поддержку оффлоадинга на несколько GPU (сейчас только single GPU)
  • Интеграцию с техниками распределения моделей на несколько карт
  • Более умное кэширование слоев (часто используемые слои остаются в VRAM)

Пока этого нет - используйте текущую реализацию. Она грубая, но работает. Как молоток: не самый изящный инструмент, но гвозди забивает.

Последний совет: не пытайтесь дообучить 70B модель на RTX 4060. Даже с оффлоадингом. Некоторые вещи требуют правильного железа. Но 32B на 8 ГБ VRAM - теперь реальность. Пусть и медленная.