ИИ-агенты и prompt engineering Дообучение: LoRA, QLoRA, инструкционный тюнинг и когда оно вообще нужно
0%

Дообучение: LoRA, QLoRA, инструкционный тюнинг и когда оно вообще нужно

Дообучение: LoRA, QLoRA, инструкционный тюнинг и когда оно вообще нужно

Все предыдущие статьи трека меняли вход модели: промпт, примеры, извлечённые документы, инструменты, цикл агента. Веса при этом оставались чужими и неизменными. Дообучение — единственная техника, которая меняет саму функцию: после него модель ведёт себя иначе даже при пустом промпте.

Это звучит как более фундаментальное решение, и именно поэтому к нему тянутся раньше времени. На практике 80 % запросов «нам нужен файнтюн» закрываются нормальным промптом, few-shot-примерами и RAG. Начнём с того, как отличить оставшиеся 20 %.

Что дообучение действительно меняет, а что нет

Полезная модель: у LLM есть три разных «канала» знания.

  1. Веса — то, что модель усвоила на претрейне: язык, факты мира, навыки рассуждения. Дообучение двигает этот канал, но очень слабо: вы прогоняете тысячи примеров против триллионов токенов претрейна.
  2. Контекст — то, что вы положили в промпт прямо сейчас. Точное, свежее, проверяемое, но платное на каждом вызове.
  3. Поведенческая политика — как модель отвечает: формат, тон, длина, когда молчит, когда зовёт инструмент. Это то, что дообучение меняет очень хорошо и очень дёшево.

Отсюда главное правило: дообучение — это инструмент про форму, а не про факты.

Эмпирическое подтверждение есть: Ovadia et al., «Fine-Tuning or Retrieval? Comparing Knowledge Injection in LLMs», arXiv:2312.05934 сравнивали закачку нового фактического знания через unsupervised-файнтюн и через RAG на MMLU-подобных задачах — RAG уверенно выигрывал, а файнтюн на фактах давал прирост только при агрессивной аугментации (много перефразировок одного факта). Модель, дообученная на вашей вики, не «выучит» её: она выучит стиль вашей вики и начнёт уверенно галлюцинировать в этом стиле.

Задача Промпт RAG Файнтюн
Ответы по свежим внутренним документам ✗ (галлюцинации в правильном стиле)
Жёсткий формат вывода (свой DSL, XML-диалект) частично
Специфичный тон бренда, единый стиль редактуры частично
Классификация в 200 доменных классов дорого частично
Дистилляция большой модели в маленькую
Снижение латентности и цены при том же качестве
Вызов инструментов по узкой схеме ✓ (если промпт уже не помогает)
Новый язык/домен с непохожей лексикой частично (нужен continued pretraining)

Правило принятия решения, которое стоит повесить на стену:

Если вы можете описать желаемое поведение словами и модель ему следует — пишите промпт. Если вы можете описать словами, но модель систематически не следует на 20+ примерах — вот здесь начинается файнтюн.

Арифметика памяти: почему полный файнтюн больно

Прежде чем говорить про LoRA, полезно понять, откуда берётся её экономия. Возьмём модель на 7 млрд параметров и стандартный mixed-precision AdamW.

Что храним Точность Байт на параметр Для 7B
Веса модели bf16 2 14 ГБ
Градиенты bf16 2 14 ГБ
Мастер-копия весов fp32 4 28 ГБ
Adam: первый момент $m$ fp32 4 28 ГБ
Adam: второй момент $v$ fp32 4 28 ГБ
Итого статики 16 112 ГБ

Плюс активации — они зависят от batch size и длины последовательности, и с gradient checkpointing для batch 1 × 2048 токенов это ещё порядка 5–8 ГБ. Итог: 7B-модель не дообучается целиком даже на H100 80 ГБ без ZeRO-шардирования по нескольким картам или offload на CPU.

Ключевое наблюдение: веса — это всего 12,5 % бюджета. Основная масса — состояния оптимизатора и градиенты, то есть плата за то, что параметр обучаемый. Значит, если сделать обучаемыми 0,5 % параметров, исчезнет 87,5 % бюджета.

Сравнение потребления видеопамяти: полный файнтюн, LoRA, QLoRA

LoRA: почему низкий ранг работает

Идея опирается на наблюдение Aghajanyan et al., «Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning», arXiv:2012.13255: адаптация предобученной модели к конкретной задаче имеет низкую внутреннюю размерность. Матрица обновления $\Delta W$, которую вы ищете при файнтюне, — «почти низкоранговая»: вы не перестраиваете представление, вы делаете небольшой поворот в подпространстве, которое уже есть.

Hu et al., «LoRA: Low-Rank Adaptation of Large Language Models», arXiv:2106.09685 превратили это в метод: заморозить $W_0$ и параметризовать обновление произведением двух тонких матриц.

$$h = W_0 x + \frac{\alpha}{r} B A x, \qquad B \in \mathbb{R}^{d \times r}, \quad A \in \mathbb{R}^{r \times k}, \quad r \ll \min(d, k)$$

Вместо $d \cdot k$ обучаемых параметров — $r(d + k)$. Для квадратной матрицы 4096×4096 и $r = 16$ это 131 тыс. против 16,8 млн: в 128 раз меньше.

Разложение LoRA: замороженная матрица плюс произведение двух низкоранговых

Три детали, которые определяют, заработает ли это.

Инициализация. $A$ — случайная гауссова, $B$ — нулевая. Значит $BA = 0$, и на первом шаге модель в точности равна предобученной. Без этого вы стартуете со случайного возмущения весов и первые сотни шагов тратите на возврат к исходной точке.

Скейлинг $\alpha/r$. Он нужен, чтобы менять $r$ без перетюна learning rate. Практическое эвристическое правило: держите $\alpha = 2r$ (или $\alpha = r$) и настраивайте только $r$. Работа Kalajdzievski, «A Rank-Stabilized Scaling Factor for LoRA», arXiv:2312.03732 показывает, что при больших $r$ деление на $r$ слишком душит градиент, и предлагает $\alpha/\sqrt{r}$ — в peft это флаг use_rslora=True. Если вы поднимаете ранг выше 64 и не видите улучшения, включите его: часто «ранг не помогает» на деле означает «скейлинг задушил обновление».

Куда вешать адаптеры. Оригинальная статья ставила их только на $W_q$ и $W_v$. Современная практика (и то, что делает QLoRA) — на все линейные слои: q_proj, k_proj, v_proj, o_proj, gate_proj, up_proj, down_proj. Разница в качестве заметная, а прирост параметров всё равно копеечный. В peft это target_modules="all-linear".

Честная оценка ограничений — Biderman et al., «LoRA Learns Less and Forgets Less», arXiv:2405.09673. Их вывод в двух пунктах: на инструкционном тюнинге LoRA практически догоняет полный файнтюн; на освоении нового домена (код, математика с миллиардами токенов) — заметно отстаёт. Зато LoRA существенно меньше разрушает исходные способности модели, то есть работает как регуляризатор. Если вам нужно влить в модель принципиально новый домен, вам нужен не LoRA-тюнинг, а continued pretraining с высоким рангом или полный файнтюн.

QLoRA: база в 4 битах

Dettmers et al., «QLoRA: Efficient Finetuning of Quantized LLMs», arXiv:2305.14314 добили последний оставшийся кусок — сами замороженные веса. Три компонента:

  1. NF4 (4-bit NormalFloat). Информационно-теоретически оптимальный тип данных для весов, распределённых нормально: уровни квантизации расставлены по квантилям нормального распределения, а не равномерно. Веса нейросетей действительно близки к $N(0, \sigma^2)$, поэтому NF4 бьёт обычный int4 при том же числе бит.
  2. Двойная квантизация. Квантизация идёт блоками (обычно 64 веса), у каждого блока свой fp32-множитель. Эти множители тоже квантуют — до 8 бит. Экономия ~0,37 бит на параметр: для 65B это полгигабайта.
  3. Paged optimizers. Состояния оптимизатора живут в unified memory NVIDIA и вытесняются в RAM при пиках — вместо OOM на длинной последовательности вы получаете просадку скорости.

Ключевой момент: веса квантованы, но не обучаются. При forward-проходе блок деквантуется в bf16 на лету, считается матричное произведение, деквантованный тензор выбрасывается. Градиенты текут сквозь квантованные веса в адаптер, который живёт в bf16. Никакого quantization-aware training — просто заморожённая 4-битная база.

Результат из статьи: 65B-модель дообучается на одной GPU 48 ГБ за 24 часа, а полученная Guanaco достигает 99,3 % от уровня ChatGPT на Vicuna-бенчмарке.

Цена, о которой говорят реже: QLoRA медленнее LoRA примерно на 25–40 % (деквантизация на каждом шаге не бесплатна) и даёт чуть более шумные градиенты. Правило: если модель влезает в bf16 — берите обычный LoRA; QLoRA — это способ поместить в железо то, что иначе не помещается.

Данные решают всё остальное

Здесь ломается большинство проектов. Метод дообучения — это конфиг на 20 строк; датасет — это несколько недель.

Формат и шаблон чата

Инструкционный тюнинг (Wei et al., «Finetuned Language Models Are Zero-Shot Learners», arXiv:2109.01652; Ouyang et al., «InstructGPT», arXiv:2203.02155) — это обучение на парах «инструкция → ответ», отформатированных ровно тем шаблоном, который модель увидит в проде.

Самая частая и самая тихая ошибка — расхождение chat template между обучением и инференсом. Если вы обучали на ### Instruction:\n...\n### Response:\n, а сервите через tokenizer.apply_chat_template() с ChatML-разметкой — модель видит другой префикс и ведёт себя как недообученная. Всегда берите шаблон из токенизатора базовой модели:

from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")

messages = [
    {"role": "system", "content": "Ты извлекаешь поля из счетов и возвращаешь JSON."},
    {"role": "user", "content": "Счёт № 42 от 03.05.2026, сумма 12 400 ₽, НДС 20%"},
    {"role": "assistant", "content": '{"number":"42","date":"2026-05-03","total":12400,"vat":0.2}'},
]

# Ровно та же строка, что попадёт в модель на инференсе
text = tok.apply_chat_template(messages, tokenize=False)
print(text)

Маскирование промпта

По умолчанию causal LM учится предсказывать все токены последовательности, включая вашу инструкцию. Обычно это не то, что нужно: вы хотите, чтобы модель училась генерировать ответ, а не воспроизводить промпт. Маскирование лоссом только completion-части (completion_only_loss в TRL) обычно даёт заметный прирост на коротких ответах и почти нулевой — на длинных. Проверяйте оба варианта, но начинайте с маскирования.

Качество против количества

Zhou et al., «LIMA: Less Is More for Alignment», arXiv:2305.11206 дообучили LLaMA-65B на 1000 тщательно отобранных примеров и получили результат, конкурентный с моделями, обученными на сотнях тысяч. Гипотеза статьи: знания приходят из претрейна, а инструкционный тюнинг лишь «включает» нужный режим — и для этого хватает малого числа очень чистых демонстраций.

Практическая шкала:

Цель Порядок числа примеров
Жёсткий формат вывода, одна узкая задача 300–1 000
Стиль/тон, доменная терминология 1 000–5 000
Классификация, много классов 50–200 на класс
Дистилляция сильной модели в маленькую 10 000–100 000
Новый домен/язык (continued pretraining) сотни миллионов токенов

Гигиена датасета, без которой цифры выше не работают:

  • Дедупликация near-duplicate’ов (MinHash/SimHash). Повторы — это скрытое переобучение на подмножестве.
  • Разделение по источнику, а не случайное. Если один документ породил 50 примеров, они обязаны целиком лежать либо в train, либо в eval. Случайный train_test_split даст вам утечку и красивую, но фальшивую метрику.
  • Проверка на контаминацию бенчмарками, на которых вы потом собираетесь мерить.
  • Единообразие «отказов». Если в 30 % примеров модель отвечает «не знаю» по-разному, вы обучаете её на шуме. Зафиксируйте одну формулировку.
  • Ошибки в разметке дороже, чем кажется. На датасете в 1000 примеров 5 % мусора — это 50 примеров, которые модель выучит буквально.

Дистилляция как основной практический сценарий

Самый окупаемый вид файнтюна в проде выглядит так: сильная модель через API размечает данные, маленькая открытая модель учится их воспроизводить. Подход восходит к Self-Instruct (Wang et al., arXiv:2212.10560).

Экономика на конкретных числах. Разметка 20 000 примеров учителем уровня Claude Opus 4.8 (input $5, output 25 $ за 1M токенов) при 600 входных и 400 выходных токенах на пример — это 12M входных и 8M выходных токенов, то есть примерно 60 $ + 200 $ = 260 $ разово. Дообучение 8B-модели QLoRA на арендованной A100 — ещё 10 $–20. Дальше вы платите только за собственный инференс. Если у вас 3 млн запросов в месяц, разница между вызовом фронтир-модели и своей 8B на своём железе измеряется десятками тысяч долларов в месяц — файнтюн окупается за первые сутки.

Обратная сторона: вы зафиксировали качество учителя на момент разметки. Через полгода выйдет модель лучше, а ваш ученик останется прежним. И проверьте условия использования API поставщика: обучение конкурирующей модели на его выходах обычно запрещено, дистилляция для внутренних задач — обычно нет.

Рабочий код

Стек по умолчанию на 2026 год: transformers + peft + trl + bitsandbytes. Ниже — минимальный, но полноценный скрипт QLoRA-тюнинга.

"""QLoRA-дообучение 7B-модели на инструкционном датасете.
Требования: одна GPU от 16 ГБ. Проверено на transformers 4.5x / trl 0.2x."""

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

BASE = "Qwen/Qwen2.5-7B-Instruct"

# 1. Квантизация базы в NF4 с двойной квантизацией.
#    compute_dtype — тип, в который деквантуются блоки на forward.
bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)

tok = AutoTokenizer.from_pretrained(BASE)
model = AutoModelForCausalLM.from_pretrained(
    BASE,
    quantization_config=bnb,
    dtype=torch.bfloat16,
    device_map={"": 0},          # никакого auto: шардирование ломает checkpointing
    attn_implementation="flash_attention_2",
)
model.config.use_cache = False   # несовместимо с gradient checkpointing

# 2. Конфиг адаптера. all-linear — все линейные слои, кроме lm_head.
peft_config = LoraConfig(
    r=32,
    lora_alpha=64,               # alpha = 2r — держим соотношение постоянным
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules="all-linear",
    use_rslora=True,             # alpha/sqrt(r) вместо alpha/r
)

# 3. Датасет в формате messages — TRL сам применит chat template токенизатора.
ds = load_dataset("json", data_files={
    "train": "data/train.jsonl",
    "test": "data/eval.jsonl",
})

args = SFTConfig(
    output_dir="out/invoice-extractor",
    num_train_epochs=2,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=8,      # эффективный батч = 32
    learning_rate=2e-4,                 # для LoRA это норма, для полного FT — катастрофа
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    optim="paged_adamw_8bit",           # paged: переживает пики на длинных примерах
    bf16=True,
    max_length=2048,
    packing=False,                      # для инструкций — без упаковки
    completion_only_loss=True,          # лосс только на ответе ассистента
    gradient_checkpointing=True,
    gradient_checkpointing_kwargs={"use_reentrant": False},
    logging_steps=10,
    eval_strategy="steps",
    eval_steps=100,
    save_steps=100,
    load_best_model_at_end=True,        # берём лучший чекпоинт, а не последний
    metric_for_best_model="eval_loss",
    seed=42,
)

trainer = SFTTrainer(
    model=model,
    args=args,
    train_dataset=ds["train"],
    eval_dataset=ds["test"],
    peft_config=peft_config,
    processing_class=tok,
)

trainer.train()
trainer.save_model()   # сохранится только адаптер: десятки мегабайт

Тот же прогон в декларативном виде — axolotl, удобен, когда конфиг нужно версионировать и запускать из CI:

base_model: Qwen/Qwen2.5-7B-Instruct
load_in_4bit: true
adapter: qlora

lora_r: 32
lora_alpha: 64
lora_dropout: 0.05
lora_target_linear: true

datasets:
  - path: data/train.jsonl
    type: chat_template
    field_messages: messages

val_set_size: 0.05
sequence_len: 2048
sample_packing: false
train_on_inputs: false        # маскируем промпт

micro_batch_size: 4
gradient_accumulation_steps: 8
num_epochs: 2
optimizer: paged_adamw_8bit
lr_scheduler: cosine
learning_rate: 0.0002
warmup_ratio: 0.03
bf16: auto
gradient_checkpointing: true
flash_attention: true
save_total_limit: 3

Гиперпараметры: с чего начинать

Параметр Стартовое значение Что делать, если плохо
r 16 для формата, 32–64 для стиля/домена Недообучение → ×2. Переобучение → ÷2 и больше данных
lora_alpha 2 $r$ Не крутите отдельно от $r$; при $r > 64$ включите use_rslora
learning_rate 1e-4 … 3e-4 (LoRA), 1e-5 … 2e-5 (полный FT) Лосс скачет или NaN → ÷3. Лосс не падает → ×2
Эпохи 2–3 1 эпоха для 50k+ примеров; 4+ почти всегда переобучение
Эффективный батч 16–64 Меньше 8 — шумные градиенты; берите accumulation, а не большую карту
lora_dropout 0.05 0.1 при малом датасете, 0 при большом
warmup_ratio 0.03 Всплеск лосса в начале → 0.1
max_grad_norm 1.0 Оставьте как есть, это спасает от единичных выбросов

Learning rate — главный источник катастроф. LoRA терпит $2 \cdot 10^{-4}$ именно потому, что обновляет крошечное подпространство; тот же lr на полном файнтюне уничтожит модель за сотню шагов.

Оценка: где прячется деградация

Loss на валидации — это не метрика качества, это индикатор того, что обучение не разошлось. Реальную оценку строим по трём осям.

1. Целевая задача. Holdout, разделённый по источнику, с метрикой, которая соответствует задаче: exact match для извлечения полей, схема-валидность для JSON, LLM-as-judge для стиля (см. оценку и бенчмарки).

2. Катастрофическое забывание. Это главный скрытый риск. Luo et al., «An Empirical Study of Catastrophic Forgetting in LLMs During Continual Fine-tuning», arXiv:2308.08747 показывают, что деградация общих способностей растёт с размером модели и с агрессивностью тюнинга. Модель, идеально извлекающая поля из счетов, может разучиться считать или следовать системному промпту.

Минимальный регресс-набор, который стоит прогонять на каждом чекпоинте: 200 задач MMLU, 50 задач HumanEval (если код важен), 100 примеров вашего предыдущего продового поведения. Если общая метрика упала больше чем на 2–3 п.п. — снижайте lr, уменьшайте число эпох или подмешивайте 5–10 % общих инструкционных данных.

3. Безопасность. Qi et al., «Fine-tuning Aligned Language Models Compromises Safety, Even When Users Do Not Intend To!», arXiv:2310.03693 — тревожный и важный результат: дообучение на совершенно безобидных данных заметно ослабляет alignment-барьеры. Если ваша модель смотрит наружу, регресс по безопасности обязателен, а не желателен. Подробнее про поверхность атаки — в статье о безопасности.

Диагностика по кривым:

  • Train падает, eval растёт — переобучение. Меньше эпох, больше dropout, больше данных.
  • Оба стоят на месте — слишком маленький lr, слишком маленький $r$, или ваш датасет не содержит сигнала.
  • Резкий скачок вверх — плохой пример в батче или слишком большой lr. Включите max_grad_norm и проверьте самые длинные примеры.
  • Eval хороший, прод плохой — почти наверняка расхождение chat template или утечка в сплите.

Жизненный цикл адаптера в продакшене

Адаптер — это артефакт с версией, а не файл в чьей-то домашней папке.

Что обязано лежать рядом с весами адаптера: хеш датасета, полный конфиг обучения, версия базовой модели (с точным ревизионным хешем с HF), метрики на holdout и на регресс-наборе, дата. Без этого через три месяца вы не сможете воспроизвести собственный результат.

Слить или не сливать

Два режима сервинга, и выбор между ними — архитектурное решение.

Merge. merged = base + (α/r)·BA записывается в веса, получается обычная модель. Плюсы: нулевой оверхед на инференсе, работает где угодно, спокойно квантуется дальше. Минусы: на каждый адаптер — полная копия модели.

from peft import PeftModel
from transformers import AutoModelForCausalLM

base = AutoModelForCausalLM.from_pretrained(BASE, dtype=torch.bfloat16)
merged = PeftModel.from_pretrained(base, "out/invoice-extractor").merge_and_unload()
merged.save_pretrained("out/invoice-extractor-merged")

⚠️ Не сливайте QLoRA-адаптер в 4-битную базу. Адаптер обучался против деквантованных весов; слияние в квантованную базу даёт ощутимую потерю качества. Правильный путь: загрузить базу в bf16, слить туда, при необходимости заново квантовать результат (GPTQ/AWQ).

Multi-LoRA. Держим одну базу в памяти и подгружаем десятки адаптеров, переключая их по запросу. Идея — Sheng et al., «S-LoRA: Serving Thousands of Concurrent LoRA Adapters», arXiv:2311.03285; в vLLM это штатная фича:

vllm serve Qwen/Qwen2.5-7B-Instruct \
  --enable-lora \
  --max-loras 8 \
  --max-lora-rank 32 \
  --lora-modules invoices=/models/invoice-extractor tone=/models/brand-tone

Дальше "model": "invoices" в теле запроса выбирает адаптер. Оверхед — единицы процентов латентности, зато один GPU обслуживает десятки задач или клиентов. Это правильная архитектура для мультитенантности: адаптер на клиента вместо модели на клиента.

Куда уходят деньги и время

Ориентиры для одного прогона (цены аренды GPU — порядок величины, они постоянно меняются):

Сценарий Железо Время Стоимость прогона
QLoRA 7B, 5k примеров, 3 эпохи 1×RTX 4090 24 ГБ 1,5–3 ч ~1 $
QLoRA 7B, 50k примеров, 2 эпохи 1×A100 80 ГБ 8–14 ч 15 $–25
LoRA 8B в bf16, 50k примеров 1×H100 80 ГБ 5–9 ч 20 $–35
QLoRA 70B, 20k примеров 2×A100 80 ГБ 30–50 ч 120 $–200
Полный FT 7B 4×A100 80 ГБ + ZeRO-3 20–30 ч 150 $–250

И честное распределение усилий проекта:

  • Сбор, чистка, разметка датасета — 60–70 %
  • Построение оценки и регресс-набора — 15–20 %
  • Собственно обучение и подбор гиперпараметров — 10 %
  • Сервинг и раскатка — 5–10 %

Если у вас наоборот — вы, скорее всего, перебираете ранги на грязных данных.

Дальше SFT: выравнивание по предпочтениям

Supervised fine-tuning учит модель воспроизводить один эталонный ответ. Иногда эталона нет, а есть суждение «этот ответ лучше того» — например, для тона поддержки или для качества саммари.

Классический путь — RLHF с обучением reward-модели и PPO — дорог и капризен. Rafailov et al., «Direct Preference Optimization», arXiv:2305.18290 показали, что оптимальная политика RLHF выражается в замкнутой форме через саму модель, и задачу можно свести к обычной классификационной функции потерь на парах «выбранный/отвергнутый» — без reward-модели и без RL-цикла. В TRL это DPOTrainer, и он тоже прекрасно работает поверх LoRA.

Более свежий вариант — ORPO (Hong et al., arXiv:2403.07691): объединяет SFT и выравнивание в один проход, добавляя к обычному лоссу штраф за отношение шансов отвергнутого ответа. Одна стадия вместо двух.

Практическая последовательность: сначала SFT, потом preference-тюнинг, и только если SFT-версия уже приемлема. DPO на плохой SFT-модели усилит её недостатки, а не исправит.

Типичные ошибки

  1. Файнтюн вместо RAG. Пытаться «залить знания» в веса. Модель выучит стиль документов и начнёт галлюцинировать в нём — это хуже, чем прямое незнание.
  2. Разные chat template при обучении и инференсе. Тихая, полная потеря эффекта дообучения. Всегда логируйте одну готовую строку с обеих сторон и сравнивайте побайтово.
  3. Случайный train/test split. Утечка через near-duplicates даёт eval-метрику, которая ничего не значит. Делите по источнику.
  4. Отсутствие регресса на общих способностях. Целевая метрика выросла, модель разучилась считать — узнаете из тикетов.
  5. Слишком много эпох. На 1000 примеров 10 эпох — это заучивание наизусть. Берите лучший чекпоинт по eval, а не последний.
  6. Адаптеры только на attention. Наследие первой статьи. all-linear почти бесплатен и заметно лучше.
  7. Слияние QLoRA-адаптера в 4-битную базу. Тихая потеря качества после «успешного» обучения.
  8. device_map="auto" при обучении. Годится для инференса, ломает gradient checkpointing и наивно шардирует модель. Явно {"": 0}.
  9. Обучение с use_cache=True. Конфликтует с checkpointing, съедает память, иногда молча ломает градиенты.
  10. Нет версионирования артефакта. Через месяц никто не знает, какой адаптер сейчас в проде и на каких данных он обучен.
  11. Выбор базы по бенчмарку, а не по лицензии. Проверьте условия использования и коммерческие ограничения до того, как потратите месяц.
  12. Файнтюн там, где хватило бы structured output. Жёсткий JSON проще получить через constrained decoding, чем через обучение.

Мини-итог

Дообучение — это способ впечатать поведение в веса, чтобы не платить за него токенами в каждом запросе. Оно отлично работает для формата, стиля, узкой классификации и дистилляции; оно плохо работает для фактов и нового домена.

Чеклист перед запуском первого прогона:

  • Промпт с few-shot проверен на 20+ примерах и систематически не справляется.
  • Задача — про поведение и форму, а не про свежие факты (иначе — RAG).
  • Собрано минимум несколько сотен чистых пар «вход → эталонный выход».
  • Датасет дедуплицирован и разделён по источнику, а не случайно.
  • Chat template берётся из токенизатора базовой модели и одинаков в train и в проде.
  • Лосс маскирован на промпте (completion_only_loss).
  • Есть holdout с метрикой задачи и регресс-набор общих способностей.
  • Для внешних систем есть регресс по безопасности.
  • Адаптеры на всех линейных слоях, $\alpha = 2r$, lr в диапазоне 1e-4…3e-4.
  • Лучший чекпоинт выбирается по eval, а не берётся последний.
  • Артефакт версионирован: хеш данных, конфиг, ревизия базовой модели, метрики.
  • Решено, как сервить: merge в bf16 или multi-LoRA через vLLM.
  • Посчитано, за сколько запросов файнтюн окупает разметку и обучение.

И контрольный вопрос на каждом ревью: «что именно эта модель теперь умеет такого, чего нельзя было получить промптом на 200 токенов?» Если ответа нет — вы дообучали зря.

Источники

Что дальше

Модель дообучена, адаптер собран, метрики зелёные — и теперь начинается самая долгая часть жизни системы: она должна работать под нагрузкой, укладываться в бюджет и быть наблюдаемой. Дальше — ИИ в продакшене: латентность, кэширование, роутинг моделей, стоимость, наблюдаемость.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов