Файн-тюнинг Llama 3 8B: Полный гайд (Unsloth + PyTorch)
Дообучаем Llama 3 8B на своих данных через QLoRA и Unsloth: актуальная установка, рабочий код на SFTConfig и сохранение адаптеров в GGUF. Хватает одной карты на 16 ГБ.
TL;DR — Кратко:
- Unsloth заявляет обучение в 2 раза быстрее и на 70% меньше VRAM без потери точности.
- QLoRA грузит модель в 4 битах: 8B занимает около 6 ГБ вместо 16.
- Параметры обучения задаются через SFTConfig, а модель и токенизатор передаются в SFTTrainer отдельно.
Базовая Llama 3 8B знает обо всём понемногу. Чтобы она отвечала в стиле вашей поддержки, писала код под ваш внутренний фреймворк или разбирала документы вашей отрасли, её нужно дообучить.
Раньше это требовало кластера. Сегодня связка QLoRA и Unsloth укладывает дообучение 8B в одну карту потребительского класса. Ниже рабочий код от установки до сохранения в GGUF.
Почему Unsloth, а не чистый Trainer
Unsloth переписал ядра PyTorch для обратного прохода вручную. По заявлению разработчиков, это даёт обучение в 2 раза быстрее при расходе VRAM на 70% меньше и без потери точности.
Последний пункт важнее первых двух: математика остаётся прежней, ускорение идёт за счёт эффективности вычислений, а не аппроксимаций. Качество адаптера не страдает.
Установка
Понадобится Linux и карта NVIDIA. Минимум 12 ГБ VRAM, комфортно от 16 ГБ.
Актуальный способ установки из документации Unsloth использует uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv unsloth_env --python 3.13
source unsloth_env/bin/activate
uv pip install unsloth --torch-backend=auto Шаг 1. Загрузка модели в 4 битах
Модель грузится квантованной, поэтому 8B занимает около 6 ГБ вместо 16 в fp16. Откуда берутся эти цифры и что ещё расходует память при обучении, разобрано в статье про VRAM.
import torch
from unsloth import FastLanguageModel
max_seq_length = 2048 # можно поднять до 8192
dtype = None # автоопределение: float16 для Tesla T4, bfloat16 для Ampere и новее
load_in_4bit = True
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/llama-3-8b-bnb-4bit",
max_seq_length=max_seq_length,
dtype=dtype,
load_in_4bit=load_in_4bit,
) Готовые 4-битные сборки лежат в репозитории Unsloth на Hugging Face. Для инструкт-версии берите unsloth/llama-3-8b-Instruct-bnb-4bit.
Шаг 2. Настройка LoRA-адаптеров
Все 8 миллиардов весов переучивать не нужно. LoRA замораживает базовую модель и обучает небольшие матрицы поверх неё, поэтому градиенты и состояния оптимизатора считаются только для адаптера. Метод описан в статье авторов QLoRA.
model = FastLanguageModel.get_peft_model(
model,
r=16, # ранг адаптера: 16, 32, 64 — рабочие значения
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="unsloth",
random_state=3407,
use_rslora=False,
loftq_config=None,
) Чем выше ранг r, тем больше ёмкость адаптера и тем больше памяти он занимает. Начинать имеет смысл с 16.
Шаг 3. Подготовка данных
Для примера берём датасет Alpaca, но структура подойдёт для любого своего JSON с полями инструкции, контекста и ответа.
from datasets import load_dataset
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
{}
### Input:
{}
### Response:
{}"""
EOS_TOKEN = tokenizer.eos_token
def formatting_prompts_func(examples):
texts = []
for instruction, inp, output in zip(
examples["instruction"], examples["input"], examples["output"]
):
texts.append(alpaca_prompt.format(instruction, inp, output) + EOS_TOKEN)
return {"text": texts}
dataset = load_dataset("yahma/alpaca-cleaned", split="train")
dataset = dataset.map(formatting_prompts_func, batched=True) EOS_TOKEN в конце каждого примера обязателен. Без него модель не научится останавливаться и будет генерировать текст до упора в лимит токенов.
Шаг 4. Запуск обучения
Параметры датасета задаются через SFTConfig, а не напрямую в SFTTrainer: там живут dataset_text_field, max_length, packing и dataset_num_proc. Токенизатор передаётся в processing_class.
from trl import SFTConfig, SFTTrainer
trainer = SFTTrainer(
model=model,
processing_class=tokenizer,
train_dataset=dataset,
args=SFTConfig(
dataset_text_field="text",
max_length=max_seq_length,
dataset_num_proc=2,
packing=False,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
warmup_steps=5,
max_steps=60, # для реального прогона замените на num_train_epochs=1
learning_rate=2e-4,
fp16=not torch.cuda.is_bf16_supported(),
bf16=torch.cuda.is_bf16_supported(),
logging_steps=1,
optim="adamw_8bit",
weight_decay=0.01,
lr_scheduler_type="linear",
seed=3407,
output_dir="outputs",
),
)
trainer_stats = trainer.train() При ошибке OOM снижайте
per_device_train_batch_sizeс 2 до 1 и компенсируйте черезgradient_accumulation_steps. Следующий рычаг — уменьшитьmax_length: расход памяти на активации растёт вместе с длиной последовательности.
Значение optim="adamw_8bit" выбрано не случайно: состояния Adam обычно занимают 8 байт на параметр, а восьмибитная версия сжимает их до 2.
Шаг 5. Проверка результата
FastLanguageModel.for_inference(model)
inputs = tokenizer(
[alpaca_prompt.format(
"Напиши функцию на Python для вычисления чисел Фибоначчи.",
"",
"", # ответ оставляем пустым, его дополнит модель
)],
return_tensors="pt",
).to("cuda")
outputs = model.generate(**inputs, max_new_tokens=64, use_cache=True)
print(tokenizer.batch_decode(outputs)) Шаг 6. Сохранение
Адаптеры весят порядка сотни мегабайт, потому что содержат только обученные матрицы, а не всю модель:
model.save_pretrained("lora_model")
# model.push_to_hub("your_name/lora_model") Для запуска через Ollama или LM Studio нужен GGUF с вшитыми весами:
model.save_pretrained_gguf("model", tokenizer, quantization_method="q4_k_m") Как поднять получившийся GGUF в Ollama с веб-интерфейсом, разобрано в гайде по Llama 3 и Docker.
Что дальше
Схема неизменна для любой своей задачи: соберите датасет в формате «инструкция — контекст — ответ», подставьте его вместо Alpaca, поднимите max_steps до полного прохода по данным.
Если 8B перестанет хватать, следующий шаг упирается в память: 70B через QLoRA требует уже 48 ГБ и выше. Что даёт переход на карты такого класса, разобрано в сравнении A100 и RTX 4090.
Нужна карта под дообучение?
RTX A6000 на 48 ГБ от 80 ₽/час или A100 80GB для моделей покрупнее. Драйверы, CUDA и Jupyter уже настроены.
Выбрать карту