Как дообучать локальные LLM в 2026 году: практическое руководство. llm.. llm. lora.. llm. lora. python.. llm. lora. python. QLoRA.. llm. lora. python. QLoRA. rag.. llm. lora. python. QLoRA. rag. Блог компании OTUS.. llm. lora. python. QLoRA. rag. Блог компании OTUS. дообучение.. llm. lora. python. QLoRA. rag. Блог компании OTUS. дообучение. дообучение LLM.. llm. lora. python. QLoRA. rag. Блог компании OTUS. дообучение. дообучение LLM. искусственный интеллект.. llm. lora. python. QLoRA. rag. Блог компании OTUS. дообучение. дообучение LLM. искусственный интеллект. локальные llm.. llm. lora. python. QLoRA. rag. Блог компании OTUS. дообучение. дообучение LLM. искусственный интеллект. локальные llm. локальные модели.. llm. lora. python. QLoRA. rag. Блог компании OTUS. дообучение. дообучение LLM. искусственный интеллект. локальные llm. локальные модели. Машинное обучение.

В 2026 году возможность дообучения локальных LLM стала реальной опцией для отдельных разработчиков и небольших команд. Это стало возможным благодаря снижению требований к видеопамяти (VRAM), развитию инструментов и расширению набора базовых моделей с открытыми лицензиями.

Если раньше адаптация LLM под узкоспециализированные задачи была доступна только хорошо финансируемым лабораториям, то теперь улучшения QLoRA и унифицированные фреймворки вроде Unsloth позволяют дообучать модели с 8 миллиардами параметров на одной потребительской видеокарте с 12 ГБ памяти. В этом руководстве разобран полный процесс: от выбора, нужен ли вообще этап дообучения (или fine-tuning), до подготовки данных, настройки и запуска обучения, оценки результатов и экспорта модели для локального вывода.

Как дообучать локальные LLM

  • Оцените, действительно ли требуется дообучение, или задачу можно решить с помощью промпт-инжиниринга или генерации с дополнением извлечения (RAG), используя подход принятия решений с учётом стоимости, задержек и приватности данных.

  • Подготовьте аппаратную и программную среду: Python, PyTorch, Unsloth и экосистему Hugging Face.

  • Соберите качественный набор данных объёмом от 500 до 10 000 примеров в форматах ChatML, ShareGPT или Alpaca, с удалением дубликатов и фильтрацией по длине.

  • Выберите базовую модель с открытой лицензией в диапазоне 7–8 млрд параметров (Llama 3.1 8B, Mistral 7B или Qwen 2.5 7B).

  • Настройте обучение с использованием QLoRA: задайте ранг, скорость обучения и целевые модули через Unsloth и SFTTrainer.

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

  • Объедините адаптеры LoRA с базовой моделью и экспортируйте результат в формат GGUF для локального вывода через llama.cpp или Ollama.

  • Оцените дообученную модель с помощью количественных метрик и качественного сравнения с базовой моделью «бок о бок».

Когда выбирать дообучение, а когда — промпт-инжиниринг или RAG

Подход к выбору: как принять правильное решение

Прежде чем переходить к процессу дообучения, имеет смысл системно сравнить три ключевых стратегии настройки поведения LLM: промпт-инжиниринг, RAG и дообучение. У каждой из них свои компромиссы по стоимости, задержкам, приватности данных, точности и сложности поддержки.

Параметр

Промпт-инжиниринг

RAG

Дообучение

Начальные затраты

Почти нулевые

Средние (пайплайн эмбеддингов, векторное хранилище)

Высокие (вычисления, подготовка датасета)

Задержка при выводе

Низкая

Выше (извлечение + генерация)

Низкая (без этапа извлечения)

Приватность данных

Зависит от поставщика API

Данные остаются локальными при self-hosted-развертывании

Данные полностью остаются локальными

Потолок точности

Ограничен размером контекста и знаниями базовой модели

Высокий для фактических данных; зависит от качества извлечения

Максимальный для поведенческой и стилистической адаптации

Сложность поддержки

Низкая (обновление промптов)

Средняя (актуализация индекса)

Выше (переобучение на новых данных)

Лучшие сценарии использования

Прототипирование, общие задачи, few-shot-сценарии

Вопросы и ответы с опорой на знания, поиск по документам

Доменные термины, строгие форматы вывода, офлайн-развертывание

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

Как понять, нужно ли дообучение на самом деле

Дообучение — оправданный выбор, если:

  • Требуется строгое и стабильное форматирование вывода, которого невозможно добиться только с помощью промптов.

  • Модель должна усвоить специализированную терминологию предметной области (юриспруденция, медицина, проприетарные кодовые базы).

  • Необходимо поведенческое выравнивание — модель должна последовательно придерживаться определённой роли и тона во всех ответах.

  • Критична низкая задержка, и этап извлечения, добавляемый RAG, недопустим.

  • Развёртывание происходит в изолированной среде без доступа к внешним API и векторным хранилищам.

Однако дообучение — избыточное решение в тех случаях, когда грамотно составленный системный промпт уже даёт нужное поведение, когда нехватку знаний можно компенсировать добавлением контекста на этапе вывода или когда датасет содержит меньше 200–300 качественных примеров и не способен заметно повлиять на поведение модели. В таких ситуациях вычислительные затраты и длительность итераций при дообучении дают всё меньшую отдачу.

Предварительные условия и требования к оборудованию

Минимальное оборудование для разных методов дообучения

Требования к оборудованию сильно различаются в зависимости от метода дообучения и размера модели.

Полное дообучение модели с 7 млрд параметров требует 48 ГБ VRAM или больше. Это означает, что понадобится NVIDIA A6000 или конфигурация с несколькими GPU. Для моделей свыше 13 млрд параметров уже необходимы многомашинное обучение или карты A100/H100 с 80 ГБ памяти.

LoRA (низкоранговая адаптация, англ. Low-Rank Adaptation) существенно сокращает число обучаемых параметров, снижая требования к VRAM до 16–24 ГБ. RTX 4090 (24 ГБ) или RTX 5090 без проблем справляются с 7B-моделями при использовании LoRA.

QLoRA ещё сильнее снижает требования — до 8–12 ГБ, за счёт квантизации базовой модели до 4-битной точности и обучения только низкоранговых адаптеров в более высокой точности.

RTX 4070 Ti (12 ГБ) или аналогичная потребительская карта подходит для моделей класса 7B–8B.

Для тех, у кого нет доступа к локальному GPU, облачные инстансы на RunPod, Lambda или Vast.ai с GPU A100 или H100 позволяют обойтись без покупки выделенного оборудования. Стоимость зависит от провайдера и типа GPU, поэтому перед запуском стоит проверить актуальные тарифы.

Настройка программного стека

Стек для дообучения в 2026 году в основном строится вокруг Python 3.11+, PyTorch 2.5+, CUDA 12.x и экосистемы Hugging Face (transformers, datasets, peft, trl). Unsloth предоставляет оптимизированные ядра обучения, которые уменьшают потребление памяти и повышают пропускную способность. Для воспроизводимости критически важно жёстко фиксировать версии зависимостей.

# Пример кода 1: Настройка окружения с зафиксированными версиями
conda create -n finetune python=3.11 -y
conda activate finetune

# Проверьте версию драйвера CUDA с помощью nvidia-smi и выберите
# подходящий URL индекса на странице https://pytorch.org/get-started/locally
pip install torch==2.5.1 --index-url https://download.pytorch.org/whl/cu124
pip install "unsloth>=2025.3,<2026.0"  # Check https://pypi.org/project/unsloth for latest stable version
pip install transformers==4.48.0 datasets==3.2.0 peft==0.14.0 trl==0.14.0
pip install bitsandbytes==0.45.0
pip install wandb tensorboard
pip install sentencepiece protobuf

# Проверьте доступность CUDA
python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}, Device: {torch.cuda.get_device_name(0)}')"

Это окружение обеспечивает стабильную основу. Пакет unsloth модифицирует внутренние компоненты Hugging Face, ускоряя обучение и снижая потребление памяти, тогда как bitsandbytes добавляет поддержку 4-битной квантизации для сценариев QLoRA и необходим для оптимизатора paged_adamw_8bit, используемого в процессе обучения.

Подготовка датасета для дообучения

Форматы и стандарты датасетов

В экосистеме дообучения 2026 года доминируют три формата данных.

  • ChatML использует структурированные токены <|im_start|> и <|im_end|> с явными ролями (system, user, assistant). Это нативный формат для большинства диалоговых моделей и предпочтительный выбор для задач следования инструкциям и разговорного дообучения.

  • ShareGPT хранит диалоги в виде списка ходов с полями from и value. Этот формат хорошо подходит для многоходовых диалогов и широко используется в общественных датасетах на Hugging Face.

  • Для одношаговых задач «инструкция — ответ» и задач классификации самым простым вариантом остаётся формат Alpaca, содержащий поля instruction, input и output.

Размер датасета критически зависит от задачи. Для простых задач классификации или форматирования заметное улучшение может быть достигнуто уже на 500–1000 примерах. Для сложных задач следования инструкциям или адаптации модели к новой предметной области требуется от 3000 до 10 000 качественных примеров. При объёме свыше 10 000 примеров отдача начинает снижаться, если только предметная область не является исключительно широкой.

Очистка и валидация данных

Редко бывает так, что исходные данные сразу приходят в нужном формате. Следующий скрипт преобразует записи из CSV или JSON в формат ChatML, удаляет дубликаты, фильтрует данные по длине токенов и формирует разбиение на обучающую и валидационную выборки.

# Пример кода 2: Подготовка датасета — преобразование CSV/JSON в ChatML
import json
import hashlib
import random
import logging
from pathlib import Path

logger = logging.getLogger(__name__)


def load_raw_data(filepath):
    """Load from CSV or JSON."""
    path = Path(filepath)
    if path.suffix == ".json":
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    elif path.suffix == ".csv":
        import csv
        with open(path, encoding="utf-8-sig") as f:  # utf-8-sig handles BOM
            reader = csv.DictReader(f)
            return list(reader)
    raise ValueError(f"Unsupported format: {path.suffix}")


def to_chatml(record, system_prompt="You are a helpful domain expert."):
    """Convert a record with 'instruction' and 'response' fields to ChatML."""
    return {
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": record["instruction"].strip()},
            {"role": "assistant", "content": record["response"].strip()},
        ]
    }


def deduplicate(records):
    """Remove exact duplicates based on instruction + response SHA-256 hash."""
    seen = set()
    unique = []
    for r in records:
        if "instruction" not in r or "response" not in r:
            logger.warning("Record missing required fields; skipping: %s", r)
            continue
        key = hashlib.sha256(
            (r["instruction"] + r["response"]).encode()
        ).hexdigest()
        if key not in seen:
            seen.add(key)
            unique.append(r)
    return unique


def filter_by_length(records, min_chars=50, max_chars=4000):
    """Remove records that are too short or too long."""
    valid = []
    for r in records:
        resp = r.get("response", "")
        if not isinstance(resp, str):
            logger.warning("Non-string response field; skipping: %s", r)
            continue
        if min_chars <= len(resp) <= max_chars:
            valid.append(r)
    return valid


def prepare_dataset(input_path, output_dir, val_ratio=0.1, system_prompt="You are a helpful domain expert."):
    random.seed(42)  # Set seed first for full reproducibility
    raw = load_raw_data(input_path)
    logger.info("Loaded %d raw records", len(raw))

    raw = deduplicate(raw)
    logger.info("After dedup: %d", len(raw))

    raw = filter_by_length(raw)
    logger.info("After length filter: %d", len(raw))

    random.shuffle(raw)
    split_idx = int(len(raw) * (1 - val_ratio))
    train_data = [to_chatml(r, system_prompt) for r in raw[:split_idx]]
    val_data = [to_chatml(r, system_prompt) for r in raw[split_idx:]]

    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)
    with open(out / "train.json", "w", encoding="utf-8") as f:
        json.dump(train_data, f)
    with open(out / "val.json", "w", encoding="utf-8") as f:
        json.dump(val_data, f)
    logger.info("Train: %d, Val: %d", len(train_data), len(val_data))


# Использование
prepare_dataset("raw_data.json", "./dataset", val_ratio=0.1)

Типичные ошибки при работе с датасетом

Переобучение на небольших датасетах — самый частый сценарий неудачи. Если примеров меньше 500, модель начинает запоминать обучающие образцы вместо того, чтобы обобщать закономерности. Ещё одна распространённая проблема — утечка меток, когда информация из ожидаемого ответа просачивается во входные данные. Это искусственно завышает метрики качества, но на выходе даёт бесполезную модель. Непоследовательное форматирование примеров — например, смешение Markdown и обычного текста или различающаяся структура ответов — заставляет модель обучаться на шуме оформления, а не на целевом поведении.

Что нужно понимать про LoRA, QLoRA и полное дообучение

Полное дообучение

При полном дообучении обновляются все параметры модели. Такой подход даёт более глубокую адаптацию, чем остальные методы, но требует пропорционально большего объёма VRAM. Одно только хранение весов требует 4 байта на параметр для fp32 или 2 байта на параметр для bf16; если добавить состояния оптимизатора (8 байт на параметр для AdamW) и буферы градиентов, получится полная оценка потребления VRAM.

Для модели на 7 млрд параметров в bf16 это означает 14 ГБ только на сами веса, а с учётом состояний оптимизатора суммарные требования вырастают до 48 ГБ и выше. Полное дообучение также несёт максимальный риск катастрофического забывания, когда модель, специализируясь, теряет свои общие способности. Такой подход оправдан только в тех случаях, когда сдвиг предметной области действительно велик, а датасет достаточно объёмен — речь обычно идёт о десятках тысяч примеров, чтобы полное обновление всех параметров имело смысл.

LoRA (низкоранговая адаптация, Low-Rank Adaptation)

LoRA замораживает веса базовой модели и добавляет небольшие обучаемые матрицы в определённые слои, как правило в матрицы проекций механизма внимания (q_proj, k_proj, v_proj, o_proj). Эти матрицы используют ранговое разложение: матрица обновления весов W аппроксимируется как произведение B·A, где A имеет форму (r × d_in), а B — (d_out × r), причём ранг r значительно меньше min(d_in, d_out).

За счёт этого число обучаемых параметров уменьшается на порядки. Ключевые гиперпараметры здесь — ранг (обычно от 8 до 64; более высокий ранг позволяет уловить более сложные изменения, но увеличивает расход памяти), alpha (масштабирующий коэффициент, который обычно делают равным рангу или 2× рангу; отношение alpha/rank работает как эффективный множитель для скорости обучения) и выбор целевых модулей.

QLoRA и улучшения 2026 года

QLoRA сочетает LoRA с 4-битной квантизацией базовой модели в формате NormalFloat (NF4), страничными оптимизаторами, которые заранее резервируют страницы оперативной памяти CPU, чтобы система могла переносить состояния оптимизатора на CPU при заполнении памяти GPU, а также с двойной квантизацией, когда квантуются и сами константы квантизации. Базовая модель загружается в 4-битной точности, а адаптеры LoRA и вычисления продолжают работать в bf16 или fp16. Такая архитектура позволяет дообучать 8B-модель, укладываясь менее чем в 10 ГБ VRAM, если длина последовательности не превышает 512 токенов, размер пакета равен 1 и включён контрольный пересчёт градиентов. При увеличении длины последовательности или размера пакета потребление VRAM растёт пропорционально.

С 2024 года экосистема QLoRA заметно повзрослела. Более широкая поддержка архитектур моделей означает, что QLoRA из коробки работает с семействами Llama 3, Mistral, Qwen 2.5, Phi-3 и Gemma 2. Оптимизации ядер в Unsloth дополнительно уменьшают расход памяти за счёт слияния операций и сокращения объёма хранимых промежуточных активаций.

Дообучение с Unsloth и Hugging Face: пошагово

Выбор базовой модели

При выборе модели в 2026 году нужно находить баланс между возможностями модели, соответствием доступному объёму VRAM и условиями лицензии.

Модели класса 7B–8B — оптимальная точка для потребительского оборудования: Llama 3.1 8B, Mistral 7B v0.3 и Qwen 2.5 7B уверенно помещаются в 12 ГБ GPU при использовании QLoRA. Llama 3.1 8B распространяется по лицензии сообщества Llama (она допускает большинство коммерческих сценариев, но перед развёртыванием стоит проверить актуальные условия на ai.meta.com/llama/license). Mistral 7B и Qwen 2.5 используют лицензию Apache 2.0.

Более крупные модели — варианты на 13B, которым требуется от 16 ГБ памяти даже с QLoRA, и модели на 70B, которым нужны несколько GPU или карты с 80 ГБ памяти даже при использовании QLoRA — тоже могут быть вариантом, если у вас есть соответствующее оборудование. Но для большинства сценариев локального дообучения именно диапазон 7B–8B с QLoRA даёт наилучший баланс между качеством и доступностью.

Примечание: Llama 3.1 8B — это модель с ограниченным доступом на Hugging Face. Перед загрузкой нужно принять лицензию Meta на huggingface.co/meta-llama/Meta-Llama-3.1-8B и пройти аутентификацию через huggingface-cli login.

Настройка запуска обучения

Следующий скрипт показывает полный запуск обучения QLoRA с использованием Unsloth и SFTTrainer из библиотеки trl:

# Пример кода 3: Полный скрипт обучения QLoRA с Unsloth
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset
import torch

# Конфигурация модели
max_seq_length = 2048
model_name = "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit"

# Загружаем модель с 4-битной квантизацией через Unsloth
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name=model_name,
    max_seq_length=max_seq_length,
    dtype=None,  # Автоопределение: bf16 на Ampere+, иначе fp16
    load_in_4bit=True,
)

# Применяем адаптеры LoRA
model = FastLanguageModel.get_peft_model(
    model,
    r=16,                          # Ранг: 8-64, выше = больше ёмкость
    lora_alpha=32,                 # Коэффициент масштабирования, обычно 2x ранга
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_dropout=0.05,
    bias="none",
    use_gradient_checkpointing="unsloth",  # Оптимизированный gradient checkpointing от Unsloth
)

# Загружаем датасет (формат ChatML)
dataset = load_dataset("json", data_files={
    "train": "./dataset/train.json",
    "validation": "./dataset/val.json",
})

# Форматируем диалоги для токенизатора и удаляем исходный столбец
def format_chat(example):
    return {"text": tokenizer.apply_chat_template(
        example["messages"], tokenize=False, add_generation_prompt=False
    )}

dataset = dataset.map(format_chat, remove_columns=["messages"])

# Аргументы обучения
training_args = TrainingArguments(
    output_dir="./output",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,       # Эффективный размер пакета = 16
    warmup_steps=50,
    num_train_epochs=3,
    learning_rate=2e-4,
    bf16=True,
    logging_steps=10,
    save_strategy="steps",
    save_steps=100,
    save_total_limit=3,                  # Предотвращает переполнение диска; сохраняет 3 последних чекпойнта
    eval_strategy="steps",
    eval_steps=100,
    load_best_model_at_end=True,         # Сохраняем лучший чекпойнт, а не последний
    metric_for_best_model="eval_loss",
    optim="paged_adamw_8bit",
    lr_scheduler_type="cosine",
    seed=42,
    report_to=[],                        # Явно пустой список; без внешней отчётности. Меняйте на ["wandb"] только для нечувствительных данных при заданном WANDB_API_KEY
)

# Инициализируем trainer
trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset["train"],
    eval_dataset=dataset["validation"],
    args=training_args,
    dataset_text_field="text",
    max_seq_length=max_seq_length,
    packing=True,                        # Упаковываем короткие примеры для повышения эффективности
)

# Запускаем обучение; необходимо соблюдать всю конфигурацию выше
trainer.train()

Примечание о приватности: если вы измените значение report_to на ["wandb"], метаданные обучения и кривые функции потерь будут отправляться на внешние серверы Weights & Biases. Для изолированных или проприетарных сценариев используйте ["tensorboard"].

Шаблоны конфигурации с комментариями для типовых сценариев — дообучение на инструкциях, многоходовый чат, классификация текста — обычно поставляются как сопутствующие файлы конфигурации. Основные параметры, которые нужно подбирать под конкретную задачу: rank (8 для простых задач форматирования, 32–64 для сложных сдвигов предметной области), learning rate (от 1e-4 до 3e-4 для QLoRA) и число эпох (1–3 для больших датасетов, 3–5 для небольших).

Запуск обучения и мониторинг

# Пример кода 4: Необязательная настройка логирования в WandB и сохранения чекпойнта

# Требуется переменная окружения WANDB_API_KEY.
# Для офлайн- или приватного использования: wandb.init(..., mode="offline")
import os
import wandb


# Инициализируем WandB только если это явно запрошено и ключ доступен
def init_wandb_if_configured(project: str, run_name: str) -> bool:
    """Returns True if WandB was initialized, False otherwise."""
    api_key = os.environ.get("WANDB_API_KEY")
    if not api_key:
        print("WANDB_API_KEY not set; skipping WandB logging.")
        return False
    wandb.init(project=project, name=run_name)
    return True


# Вызывается перед trainer.train() в сессии из примера 3, не как отдельный скрипт
init_wandb_if_configured("llm-finetune", "llama3.1-8b-qlora-domain")

# trainer должен быть определён в той же сессии (из примера 3)
# Сохраняем финальный чекпойнт
trainer.save_model("./output/final_checkpoint")
tokenizer.save_pretrained("./output/final_checkpoint")

print("Training complete. Final checkpoint saved.")

Ожидаемая длительность обучения зависит от оборудования. На RTX 4090 (24 ГБ) при датасете из 5000 примеров и конфигурации выше обучение в течение 3 эпох при длине последовательности 2048 обычно занимает 1–2 часа. RTX 4070 Ti (12 ГБ) будет работать дольше из-за меньшего размера пакета и большего числа шагов накопления градиента — как правило, 2–4 часа для того же датасета.

Мониторинг функции потерь на обучающей и валидационной выборках через WandB или TensorBoard критически важен для раннего выявления переобучения. Если обучающая ошибка продолжает снижаться, а валидационная растёт, это сигнал к тому, что обучение нужно остановить или уменьшить число эпох.

Если обучающая ошибка продолжает снижаться, а валидационная растёт, это сигнал к тому, что обучение нужно остановить или уменьшить число эпох.

Объединение адаптеров и экспорт модели

После завершения обучения адаптеры LoRA существуют как отдельные файлы весов. Для развёртывания их нужно объединить с базовой моделью и при необходимости преобразовать в формат GGUF для движков вывода, таких как llama.cpp или Ollama. Для экспорта в GGUF нужны либо локально установленные инструменты сборки llama.cpp, либо соответствующие дополнительные компоненты Unsloth; ориентируйтесь на документацию Unsloth для установленной у вас версии.

# Пример кода 5: Объединение весов LoRA и экспорт в GGUF
import torch
from pathlib import Path
from unsloth import FastLanguageModel

checkpoint_path = Path("./output/final_checkpoint")
assert checkpoint_path.exists(), f"Checkpoint not found: {checkpoint_path}"

# Проверяем, что это объединённый чекпойнт, а не только адаптеры
adapter_config = checkpoint_path / "adapter_config.json"
assert not adapter_config.exists(), (
    "final_checkpoint appears to contain LoRA adapters only. "
    "Run save_pretrained_merged first, then reload from merged_model path."
)

# Повторно загружаем модель — для объединения без потерь она должна быть загружена в 16 бит, а не в 4
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name=str(checkpoint_path),
    max_seq_length=2048,
    dtype=torch.bfloat16,
    load_in_4bit=False,  # Для объединения без потерь в 16 бит должно быть False
)

# Объединяем веса LoRA с базовой моделью
model.save_pretrained_merged(
    "./output/merged_model",
    tokenizer,
    save_method="merged_16bit",  # Объединённые веса в полной точности
)

# Экспортируем в GGUF для llama.cpp / Ollama
model.save_pretrained_gguf(
    "./output/gguf_model",
    tokenizer,
    quantization_method="q4_k_m",  # Варианты: q4_k_m, q5_k_m, q8_0
)

print("Model merged and exported to GGUF format.")

Выбор метода квантизации влияет и на размер модели, и на её качество. Q4_K_M даёт наилучшее соотношение размера и качества для большинства сценариев развёртывания — около 4,5 бита на один вес. Q5_K_M даёт немного более высокую точность — примерно 5,5 бита на вес. Q8_0 сохраняет почти полное качество при 8 битах на вес, но по сравнению с Q4_K_M увеличивает размер файла почти вдвое. Для модели класса 8B ожидаемый размер файлов GGUF составляет примерно 4,9 ГБ для Q4_K_M, 5,7 ГБ для Q5_K_M и 8,5 ГБ для Q8_0. Размеры масштабируются пропорционально числу параметров.

Оценка дообученной модели

Количественная оценка

Оценка должна сочетать общие метрики языкового моделирования со специализированными метриками под конкретную задачу. Перплексия (perplexity) на отложенной валидационной выборке даёт базовый ориентир: чем ниже перплексия, тем лучше модель предсказывает данные валидации, хотя напрямую качество решения задачи она не измеряет. Гораздо важнее метрики, связанные с самой задачей. Для задач классификации стоит измерять F1-меру и точность точного совпадения. Для задач генерации BLEU и ROUGE оценивают пересечение n-грамм, но их связь с человеческими предпочтениями для открытой генерации довольно слабая; поэтому по возможности лучше использовать автоматические метрики, привязанные к задаче, например LLM в роли судьи (LLM-as-judge), или ручную оценку.

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

Качественное тестирование и редтиминг

Ручное сравнение с базовой моделью на одинаковых промптах позволяет увидеть изменения в поведении, которые не отражаются в метриках. Следующий скрипт прогоняет одни и те же запросы через обе модели и выводит результаты для сравнения бок о бок. Чтобы избежать ошибок нехватки памяти на потребительских GPU, модели загружаются и тестируются последовательно.

# Пример кода 6: Сравнение вывода двух моделей бок о бок
import torch
from unsloth import FastLanguageModel


def run_inference(model, tokenizer, messages, device="cuda"):
    """Tokenize, generate, and decode only new tokens."""
    inputs = tokenizer.apply_chat_template(
        messages,
        tokenize=True,
        return_tensors="pt",
        add_generation_prompt=True,
    ).to(device)
    attention_mask = (inputs != tokenizer.pad_token_id).long().to(device)
    with torch.no_grad():
        output = model.generate(
            input_ids=inputs,
            attention_mask=attention_mask,
            max_new_tokens=256,
            pad_token_id=tokenizer.eos_token_id,
        )
    # Декодируем только вновь сгенерированные токены, без промпта
    new_tokens = output[0][inputs.shape[-1]:]
    return tokenizer.decode(new_tokens, skip_special_tokens=True)


# Тестовые промпты
test_prompts = [
    "Explain the key differences between LoRA and full fine-tuning.",
    "Generate a compliance report summary for Q3 2025.",
    # Медицинские промпты включены только для проверки деградации в предметной области;
# не развёртывайте дообученные модели для медицинских рекомендаций без клинической проверки.
    "What are the side effects of metformin?",
]

# # Загружаем и тестируем модели последовательно, чтобы избежать OOM на потребительских GPU
for model_name, label in [
    ("unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit", "BASE MODEL"),
    ("./output/final_checkpoint", "FINE-TUNED"),
]:
    model, tokenizer = FastLanguageModel.from_pretrained(
        model_name=model_name,
        max_seq_length=2048,
        load_in_4bit=True,
    )
    FastLanguageModel.for_inference(model)

    for prompt in test_prompts:
        messages = [{"role": "user", "content": prompt}]
        result = run_inference(model, tokenizer, messages)
        print(f"
{'='*60}")
        print(f"PROMPT: {prompt}")
        print(f"
{label}:
{result}")

    # Освобождаем VRAM перед загрузкой следующей модели
    del model
    torch.cuda.empty_cache()

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

Устранение проблем и лучшие практики

Типичные сценарии сбоев

Если функция потерь не хочет сходиться, самая частая причина — слишком высокая скорость обучения. Для QLoRA разумно начинать с 2e-4 и снижать до 1e-4, если loss ведёт себя нестабильно. Также стоит проверить форматирование датасета: некорректные шаблоны ChatML приводят к тому, что модель обучается на мусорных токенах.

Модель хорошо показывает себя в предметной области, но хуже справляется с общими задачами? Это катастрофическое забывание. Уменьшите число эпох, снизьте скорость обучения или уменьшите ранг LoRA. Сохранить общие способности помогает добавление в обучающий набор небольшой доли общих инструктивных данных — порядка 5–10%.

Если обучающая ошибка стремится к нулю, а валидационная растёт, это классический признак переобучения. Уменьшите число эпох, увеличьте dropout или расширьте датасет. Если примеров меньше 1000, обычно достаточно держать число эпох на низком уровне — от 1 до 3.

При ошибках CUDA OOM сначала уменьшайте размер пакета, а затем увеличивайте число шагов накопления градиента, чтобы сохранить эффективный размер пакета. Включите контрольный пересчёт градиентов — в конфигурации Unsloth выше он уже задан. Если памяти всё равно не хватает, уменьшайте длину последовательности или ранг LoRA.

Советы по подбору гиперпараметров

Для скорости обучения полезно быстро прогнать небольшой набор значений — 1e-4, 2e-4 и 3e-4 — на малой подвыборке данных (10% обучающего набора, 1 эпоха). Это позволяет быстро понять нужный порядок величины.

Выбор ранга зависит от сложности задачи: ранг 8 подходит для изменений формата и стиля, 16–32 — для умеренного сдвига предметной области, 64 — для существенного внедрения новых знаний.

Число эпох лучше держать небольшим: 1–3 для датасетов объёмом более 5000 примеров и 3–5 для меньших наборов.

Если результаты плохие, опубликованные абляционные исследования показывают, что увеличение объёма и качества датасета почти всегда даёт больший эффект, чем тонкая настройка гиперпараметров.

Если результаты плохие, опубликованные абляционные исследования показывают, что увеличение объёма и качества датасета почти всегда даёт больший эффект, чем тонкая настройка гиперпараметров.

Как дообучать локальные LLM в 2026 году: практическое руководство - 1

Если после статьи осталось ощущение, что «вроде понятно, но руками не пробовал», это нормальная стадия. Мы как раз проводим пару открытых занятий, где можно спокойно разобрать базовые вещи, посмотреть на формат обучения и задать любые вопросы — без обязательств. Заодно это удобный способ понять, насколько вам вообще откликается тема и уровень.

  • 29 апреля в 20:00. «Деревья решений для задач классификации и регрессии».
    Разберёте, как устроены решающие деревья и где они реально применяются. Будет практика: попробуете обучить модель и посмотреть, как она ведёт себя на данных. Записаться

  • 20 мая в 18:00. «Препарируем рекомендательные системы методами ML». Пройдёте по классическим подходам к рекомендациям и соберёте простой вариант своими руками. Заодно станет понятнее, как такие системы устроены под капотом и где начинаются реальные сложности. Записаться

Если хочется выстроить системное обучение и дойти до уровня специалиста по Data Science — с понятной базой, практикой и последовательным усложнением — это как раз про специализацию «Машинное обучение» со стартом 28 мая.

Автор: kmoseenk

Источник