Fine-tuning и веса · Fine-tuning and weights · урок 4 · 14 мин
Прогон обучения: TRL, Unsloth, железо, кривые loss
A training run: TRL, Unsloth, hardware, loss curves
Зачем
После урока ты запустишь SFT с LoRA в десяток строк, прикинешь, поместится ли модель на твою GPU, и прочитаешь кривые train и eval loss: где остановиться, где переобучение, где сломаны данные. На собеседовании это вопросы «what can you train on a 32 GB GPU?» и «your eval loss goes up — what now?».
Инструменты: кто что делает
| Инструмент | Для чего |
|---|---|
| TRL (Hugging Face) | trainers для post-training: SFTTrainer, DPOTrainer; эталонная реализация |
| PEFT | LoRA и другие adapters: LoraConfig, слияние, переключение |
| Unsloth | ускоренное обучение на одной GPU; документация TRL пересказывает заявление «up to 2× faster with up to 70% less VRAM» (VRAM — память GPU) |
| Axolotl | обучение по одному YAML-файлу конфигурации, multi-GPU через FSDP2 и DeepSpeed (способы разложить модель и состояния оптимизатора по нескольким GPU) |
| LLaMA-Factory | единая обвязка для многих моделей с веб-интерфейсом |
Выбор простой. Чтобы понять механику и иметь эталон — TRL с PEFT. Чтобы выжать одну GPU — Unsloth, он совместим с тем же форматом данных. Чтобы гонять много конфигураций на нескольких GPU и хранить их в git — Axolotl. Все они делают одно и то же: chat template, loss на ответе, LoRA, сохранение adapter (файла обученной добавки).
Минимальный прогон на TRL
Ниже иллюстрация по документации TRL v1.15.0 (открыта 10.10.2026). Данные — два файла JSONL в формате messages (урок a6-3).
import torch
from datasets import load_dataset
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer
data = load_dataset("json", data_files={"train": "train.jsonl", "validation": "val.jsonl"})
trainer = SFTTrainer(
model="Qwen/Qwen3-8B",
train_dataset=data["train"],
eval_dataset=data["validation"],
peft_config=LoraConfig(r=16, lora_alpha=32, lora_dropout=0.05,
target_modules="all-linear", task_type="CAUSAL_LM"),
args=SFTConfig(
output_dir="adapters/style-v1",
model_init_kwargs={"dtype": torch.bfloat16},
learning_rate=1e-4,
num_train_epochs=2,
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
max_length=2048,
assistant_only_loss=True,
eval_strategy="steps",
eval_steps=50,
logging_steps=10,
),
)
trainer.train()
trainer.save_model() # с PEFT сохраняется только adapter
Что здесь не по умолчанию и почему:
- model_init_kwargs={"dtype": torch.bfloat16}. Если модель передана строкой, TRL по документации грузит её в float32 (4 байта на вес) — вдвое больше памяти, чем bf16 (2 байта на вес).
- learning_rate=1e-4. Скорость обучения по умолчанию в SFTConfig — 2e-5, это под полное дообучение; для adapters TRL советует около 1e-4, гайд Unsloth предлагает стартовать с 2e-4.
- per_device_train_batch_size=2 и gradient_accumulation_steps=8 дают effective batch (эффективный размер пакета) 2 × 8 = 16. Накопление градиентов позволяет держать в памяти маленький batch (пакет примеров за шаг). Unsloth рекомендует именно такую стартовую точку.
- max_length=2048: по умолчанию 1 024 tokens, длинные диалоги обрезались бы вместе с ответом.
- assistant_only_loss=True: loss (ошибка обучения) только на ответах ассистента.
- eval_strategy="steps" и eval_steps=50: каждые 50 шагов считается eval loss на validation — без этого ты обучаешь вслепую.
gradient_checkpointing в SFTConfig включён по умолчанию: он не хранит часть activations (промежуточных значений прохода) и пересчитывает их, экономя память ценой времени.
Перед настоящим прогоном полезны две дешёвые проверки. Первая: распечатать один пример после подготовки данных и глазами убедиться, что chat template применён, маска loss закрывает вопрос пользователя, а в конце ответа стоит правильный token конца реплики. Вторая: обучить на 50 примерах и убедиться, что train loss быстро падает почти до нуля. Если модель не может заучить даже 50 примеров, ошибка в коде или данных, и большой прогон её не исправит. Обе проверки занимают минуты, а сэкономить могут ночь обучения на GPU.
Железо: что куда помещается
Грубое правило памяти без activations: полное дообучение с Adam — около 18 байт на параметр, LoRA с базой в bf16 — около 2, QLoRA с базой в 4 битах — около 0,5–0,6. Ориентиры из гайда Unsloth для Qwen3.5, bf16 LoRA (открыт 10.10.2026):
| Модель | VRAM для bf16 LoRA |
|---|---|
| Qwen3.5-4B | 10 ГБ |
| Qwen3.5-9B | 22 ГБ |
| Qwen3.5-27B | 56 ГБ |
| Qwen3.5-35B-A3B (MoE) | 74 ГБ |
Последняя строка — ловушка. MoE (mixture of experts, модель из многих «экспертов», из которых на каждый token работает малая часть) Qwen3.5-35B-A3B имеет 35B параметров всего и 3B активных. Вычисления на token идут как у 3B, но в памяти лежат все 35B: router (маршрутизатор) может выбрать любого эксперта. Поэтому 74 ГБ, почти как у плотной 35B. Формула для интервью: «active parameters set the compute, total parameters set the memory».
Если память кончилась (OOM, out of memory): уменьши per_device_train_batch_size и подними накопление, сократи max_length, проверь, что база в bf16, а не float32. Activations растут с длиной последовательности, поэтому длинные диалоги — частая причина OOM даже тогда, когда веса помещаются.
Как читать кривые loss
TRL пишет в журнал loss (train loss, средняя cross-entropy, то есть мера «удивления» модели правильным token, по tokens ответа за интервал), eval_loss, mean_token_accuracy, grad_norm (норма градиента) и learning_rate. Условный пример: 3 000 примеров, effective batch 16 — это около 188 шагов на эпоху (полный проход по данным), 2 эпохи — около 375 шагов.
| Шаг | train loss | eval loss |
|---|---|---|
| 50 | 1,42 | 1,38 |
| 100 | 1,21 | 1,25 |
| 150 | 1,10 | 1,19 |
| 200 | 0,95 | 1,18 |
| 250 | 0,80 | 1,21 |
| 300 | 0,66 | 1,26 |
До шага 150 обе кривые падают — модель учится. Около шага 190 начинается вторая эпоха, и train loss резко идёт вниз: модель второй раз видит те же примеры и начинает их запоминать. Eval loss после шага 200 растёт — это overfitting (переобучение): модель заучивает train вместо того, чтобы обобщать. Решение — взять checkpoint (сохранённое состояние) около шага 200 (load_best_model_at_end=True, metric_for_best_model="eval_loss", а save_steps поставить равным eval_steps, иначе нужного checkpoint просто не будет) или учить одну эпоху.
Другие узоры:
- train loss стоит высоко и не падает — слишком малая скорость обучения, сломанная маска loss или не тот chat template;
- скачки, NaN, взрывы grad_norm — слишком большая скорость обучения или битые очень длинные примеры;
- loss почти сразу падает к нулю — утечка или дубли, когда модель видит почти те же тексты; гайд Unsloth считает train loss ниже 0,2 признаком вероятного переобучения;
- eval loss хороший, а ответы плохие — loss меряет «удивление» tokens, а не формат, тон и безопасность.
Последний пункт главный: кривая loss говорит, когда остановиться, но не говорит, стала ли модель лучше. Это решает оценка против baseline (урок a6-6).
Схема
Термины · Key terms
- TRL
- библиотека Hugging Face с trainers для SFT, DPO и других методов
- PEFT
- библиотека adapters, в том числе LoRA
- learning rate
- скорость обучения, размер шага обновления весов
- effective batch size
- batch на устройство × шаги накопления градиента
- gradient accumulation
- сложение градиентов нескольких малых пакетов перед шагом
- gradient checkpointing
- пересчёт части activations вместо хранения, экономия памяти
- epoch
- один полный проход по обучающим данным
- eval loss
- ошибка на validation, которой модель не обучалась
- overfitting
- переобучение: train loss падает, eval loss растёт
- checkpoint
- сохранённое состояние модели на шаге обучения
- MoE (mixture of experts)
- модель из экспертов; память по всем, вычисления по активным
Сделай сам
Посчитай на бумаге для своего будущего прогона: 2 400 примеров, per_device_train_batch_size=4, gradient_accumulation_steps=4, 3 эпохи. Сколько шагов на эпоху и всего? При eval_steps=25 сколько будет точек eval loss? Затем нарисуй от руки две кривые: train падает всё время, eval падает до середины второй эпохи и потом растёт. Отметь шаг, который ты бы взял.
Обсуди с тьютором: покажи расчёт и рисунок; спроси, что ещё, кроме числа эпох, можно поменять, если eval loss начинает расти рано, и в каком порядке это пробовать.
Проверь себя · ответ под вопросом
1Почему Qwen3.5-35B-A3B требует для LoRA почти столько же памяти, сколько плотная 35B, хотя активных параметров всего 3B?
2Что означает рост eval loss при падающем train loss и что делать?
3Зачем в примере указан model_init_kwargs={"dtype": torch.bfloat16}?
Interview questions · US / UK
1What can you realistically fine-tune on a single 32 GB GPU?
2Your train loss keeps falling but eval loss started rising at epoch two. What do you do?
3Walk me through the key settings of a LoRA SFT run.
Ключевые фразы · Say it aloud
- Active parameters set the compute; total parameters set the memory.Активные параметры задают вычисления, общие — память.
- Adapters usually take a higher learning rate than full fine-tuning.Для adapters скорость обучения обычно выше, чем при полном дообучении.
- Effective batch size is batch per device times accumulation steps.Эффективный batch — batch на устройство × шаги накопления.
- Rising eval loss with falling train loss means overfitting.Eval loss растёт, train падает — переобучение.
- Loss tells me when to stop, not whether the model got better.Loss говорит, когда остановиться, а не стала ли модель лучше.
- I save the adapter with the data hash and the config.Adapter сохраняю вместе с хэшем данных и конфигурацией.
Материалы
- TRL — SFT Trainer — разделы «Train adapters with PEFT», «Logged metrics», справка
SFTConfig(12 мин) - Unsloth — LoRA Hyperparameters Guide — rank, alpha, learning rate, эпохи, признаки переобучения (10 мин)
- Unsloth — Qwen3.5 fine-tune — VRAM по размерам, оговорка про QLoRA (5 мин)
- Transformers — GPU memory usage — откуда 18 байт на параметр и почему activations дают OOM (5 мин)