← LLM fine-tuning: LoRA, QLoRA, and when not to do it
03 / 07 · 60 min

LoRA and QLoRA: tuning on a single GPU

Why a full 7B fine-tune needs over 100 GB, how LoRA reduces the job to a pair of small matrices, what 4-bit NF4 and double quantization buy — and a working peft + trl script for a single card.

Lesson content is currently available in Russian.

What you will learn

  • →Считать память для полного fine-tune, LoRA и QLoRA модели 7B и обосновывать выбор цифрами.
  • →Выбирать rank, alpha и target_modules и понимать их влияние на качество и память.
  • →Запускать QLoRA-обучение на одной карте через peft, bitsandbytes и trl.

Арифметика памяти: почему 7B не влезает

Начнём с числа, которое отрезвляет. Модель на 7 миллиардов параметров в fp16 занимает 14 ГБ — по 2 байта на параметр. Кажется, что 24-гигабайтной карты хватит для обучения. Не хватит. Полный fine-tune с оптимизатором AdamW хранит для каждого обучаемого параметра: сам вес (2 байта), градиент (2 байта) и два момента оптимизатора в fp32 (8 байт), плюс обычно fp32-копию весов (4 байта). Итого порядка 16 байт на параметр — около 112 ГБ для 7B ещё до активаций. Это территория нескольких A100, недоступная одиночке.

LoRA разрубает узел: базовые веса замораживаются — для них не нужны ни градиенты, ни состояния оптимизатора. Обучаются только маленькие адаптеры. Для 7B с rank 16 на проекциях внимания и MLP это порядка 40 миллионов параметров — около 0,5% модели. Память: 14 ГБ замороженной базы плюс менее гигабайта на адаптеры с их градиентами и оптимизатором. С gradient checkpointing обучение помещается в 24 ГБ.

QLoRA делает последний шаг: замороженную базу можно хранить не в fp16, а в 4 битах — около 3,5–4 ГБ для 7B. Обучение 7–8B влезает в бесплатный Colab с T4 на 16 ГБ, а авторы статьи QLoRA дообучали 65B на одной карте в 48 ГБ. Так тюнинг перестал требовать кластера.

LoRA: низкоранговое обновление и его ручки

Идея LoRA: вместо изменения матрицы весов W целиком учим её низкоранговую поправку. Обновлённый слой считает W·x + (alpha/r)·B·A·x, где A — матрица r×k, B — матрица d×r, а rank r много меньше размерностей слоя. A инициализируется случайно, B — нулями: в момент старта поправка равна нулю, и модель в точности совпадает с базовой — обучение начинается из стабильной точки.

Главные ручки. Rank r (типично 8–64) — ёмкость поправки: больше rank — больше выразительность и больше параметров. На практике отдача быстро насыщается: r=16 — разумный старт, r=64 редко даёт заметный прирост на узких задачах. Alpha — множитель масштаба: эффективная сила адаптера пропорциональна alpha/r; распространённая практика — alpha, равное r или 2r (например, r=16, alpha=32), и подбор learning rate вместо перебора alpha. target_modules — на какие слои вешать адаптеры: минимум — проекции внимания q_proj, k_proj, v_proj, o_proj; авторы QLoRA показали, что покрытие всех линейных слоёв (включая gate/up/down_proj в MLP) влияет на качество сильнее, чем повышение rank. В peft это одна строка: target_modules="all-linear".

Режим обучения тоже свой: learning rate 1e-4–2e-4 — на порядок выше, чем при полном fine-tune, 1–3 эпохи, lora_dropout 0,05. Больше трёх эпох на малом датасете — почти всегда переобучение.

QLoRA: NF4, double quantization и paged optimizers

QLoRA — это LoRA поверх базы, сжатой до 4 бит, плюс три инженерных приёма из статьи.

Первый — тип данных NF4 (NormalFloat4). Веса обученных сетей распределены примерно нормально, а обычная 4-битная сетка (int4, fp4) расставляет уровни равномерно и тратит их на редкие хвосты. NF4 расставляет 16 уровней по квантилям нормального распределения — каждый уровень покрывает равную вероятностную массу, и точность на реальных весах выше при тех же 4 битах.

Второй — double quantization: веса квантуются блоками, у каждого блока своя константа масштаба в fp32; при блоке в 64 веса эти константы сами занимают заметную память, поэтому их квантуют вторым проходом. Экономия — около 0,37 бита на параметр, примерно 0,3 ГБ на 7B: немного, но именно эти сотни мегабайт часто решают, влезет ли обучение в карту.

Третий — paged optimizers: состояния оптимизатора при пиках памяти (длинные последовательности) выгружаются в RAM через unified memory, спасая от внезапных OOM.

Важно понимать механику: 4-битными веса только хранятся. При каждом умножении блок деквантуется в bf16 на лету, и вся арифметика идёт в bf16 (bnb_4bit_compute_dtype). Отсюда цена: QLoRA на 30–40% медленнее fp16-LoRA. По качеству статья показывает паритет с 16-битным fine-tune на их бенчмарках — компромисс почти всегда в пользу памяти.

Рабочий скрипт: peft + trl + bitsandbytes

Собираем всё в минимальный рабочий скрипт для одной карты:

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer

bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
    "Qwen/Qwen2.5-7B-Instruct", quantization_config=bnb, device_map="auto"
)

peft_config = LoraConfig(
    r=16, lora_alpha=32, lora_dropout=0.05,
    target_modules="all-linear", task_type="CAUSAL_LM",
)

trainer = SFTTrainer(
    model=model,
    train_dataset=ds,
    peft_config=peft_config,
    args=SFTConfig(
        num_train_epochs=2, learning_rate=2e-4,
        per_device_train_batch_size=2, gradient_accumulation_steps=8,
        gradient_checkpointing=True, bf16=True, logging_steps=10,
    ),
)
trainer.train()
trainer.save_model("out/adapter")  # адаптер — десятки мегабайт, не гигабайты

Эффективный batch size здесь 16 (2 × 8 шагов аккумуляции) — стабильный минимум для SFT. Следите за кривой loss: здоровый прогон — быстрое падение в начале и плавное выполаживание; loss около нуля — признак дубликатов или утечки eval-данных в train. Сохранённый адаптер подключается к базе через PeftModel.from_pretrained и весит десятки мегабайт — базу копировать не нужно.

Если скорость критична, посмотрите Unsloth: оптимизированные ядра дают ускорение примерно вдвое и заметную экономию VRAM на тех же конфигурациях при совместимом API.

Practice

QLoRA-прогон на своём датасете

Дообучите модель 7–8B на датасете из прошлого урока: QLoRA-конфиг с обоснованными r/alpha/target_modules, обучение на одной карте (Colab T4 подойдёт), сохранение адаптера и сравнение генераций до и после.

Deliverable

Сохранённый адаптер, график loss, таблица памяти «полный FT / LoRA / QLoRA» для вашей модели и 5 пар генераций base против tuned на held-out промптах.

Checkpoint

Защитите свой конфиг обучения: почему выбраны эти r, alpha, target_modules и learning rate — и куда ушла память.

  • ✓ Посчитана память для полного FT, LoRA и QLoRA вашей модели, выбор обоснован цифрами.
  • ✓ Обоснованы r, alpha и target_modules, learning rate соответствует LoRA-диапазону.
  • ✓ Обучение завершилось без OOM, адаптер сохранён отдельно и подключается к базовой модели.

Check yourself

Почему полный fine-tune 7B требует порядка 112 ГБ, если веса занимают 14 ГБ?

01Почему полный fine-tune 7B требует порядка 112 ГБ, если веса занимают 14 ГБ?

Зачем матрица B в LoRA инициализируется нулями?

02Зачем матрица B в LoRA инициализируется нулями?

Чем NF4 лучше равномерного int4 для весов LLM?

03Чем NF4 лучше равномерного int4 для весов LLM?

Что экономит double quantization в QLoRA?

04Что экономит double quantization в QLoRA?

Типичный learning rate для LoRA-SFT?

05Типичный learning rate для LoRA-SFT?

Sources

LoRA and QLoRA: tuning on a single GPU · LLM fine-tuning: LoRA, QLoRA, and when not to do it · VORCL