Qwen3-Coder уже умеет:
- писать и объяснять программный код;
- искать ошибки;
- рефакторить проекты;
- работать с несколькими файлами;
- выполнять инструкции;
- использовать инструменты;
- анализировать большие репозитории.
- внутренних правилах вашего проекта;
- принятом стиле программирования;
- собственных библиотеках;
- структуре приватного репозитория;
- специальных форматах конфигурации;
- внутренних API;
- правилах оформления ответов;
- типичных задачах конкретной команды.
Технология называется LoRA — Low-Rank Adaptation, а вариант с загрузкой базовой модели в 4-битном виде — QLoRA.
LoRA позволяет не изменять все миллиарды параметров модели, а обучить сравнительно небольшой набор дополнительных матриц — адаптер.
Упрощённая схема:
Код:
Базовая Qwen3-Coder
+
Обученный LoRA-адаптер
=
Модель, адаптированная под вашу задачу
В результате можно создать отдельные адаптеры:
Код:
Qwen3-Coder
├── LoRA для Python
├── LoRA для системного программирования
├── LoRA для анализа логов
├── LoRA для безопасного кода
├── LoRA для внутреннего API
└── LoRA для определённого стиля ответов
---
# Важное уточнение: это не обучение модели с нуля
LoRA не создаёт новую модель.
Она адаптирует уже обученную Qwen3-Coder.
Есть несколько разных процессов:
| Метод | Что происходит |
|---|---|
| Предобучение с нуля | Создаётся новая модель на огромном корпусе |
| Continued pretraining | Модель продолжает изучать сырой текст или код |
| SFT | Модель обучается отвечать на инструкции |
| LoRA | Обучается небольшой адаптер поверх базовой модели |
| QLoRA | Базовая модель загружается в 4-битном виде, обучается LoRA |
| DPO | Модель учится предпочитать хорошие ответы плохим |
| RAG | Документы передаются модели во время запроса без обучения |
В этом руководстве рассматривается:
Код:
Supervised Fine-Tuning
+
QLoRA
То есть мы подготовим пары:
Код:
Инструкция пользователя → хороший ответ модели
и обучим адаптер воспроизводить желаемое поведение.
---
# Когда использовать LoRA, а когда RAG
LoRA подходит, если необходимо изменить:
- стиль ответа;
- формат кода;
- последовательность действий;
- правила оформления;
- привычный способ решения задач;
- терминологию;
- поведение модели;
- устойчивый шаблон ответа.
- документацию;
- новые статьи;
- внутреннюю базу знаний;
- описание API;
- быстро меняющиеся данные;
- большой набор справочных материалов.
Код:
Задача:
Научить модель всегда добавлять обработку ошибок.
Решение:
LoRA
Код:
Задача:
Дать модели документацию внутреннего API из 10 000 страниц.
Решение:
RAG
Код:
Задача:
Научить модель отвечать в определённом стиле
и одновременно использовать внутреннюю документацию.
Решение:
LoRA + RAG
Не следует пытаться «записать» постоянно обновляемую документацию в LoRA. После каждого изменения пришлось бы заново обучать адаптер.
---
# Какую модель использовать
В статье используется:
Код:
Qwen/Qwen3-Coder-30B-A3B-Instruct
Основные характеристики:
Код:
Архитектура: Mixture of Experts
Всего параметров: около 30,5 млрд
Активных параметров: около 3,3 млрд
Количество слоёв: 48
Количество экспертов: 128
Активных экспертов: 8
Исходный контекст: 262 144 токена
Режим: Instruct, без thinking-блоков
Лицензия: Apache 2.0
Несмотря на 3,3 млрд активных параметров, в память необходимо загрузить веса всей модели.
Это важно:
MoE снижает вычислительную стоимость одного токена, но не уменьшает объём всех хранимых весов до 3,3 млрд параметров.
---
# LoRA и QLoRA: в чём разница
## LoRA
При обычной LoRA базовая модель загружается в BF16 или FP16.
Преимущества:
- меньше потерь от квантования;
- проще последующее объединение;
- обычно выше скорость;
- стабильное обучение.
Недостаток:
- Qwen3-Coder-30B требует много видеопамяти.
Вес базовой модели в BF16 составляет приблизительно:
Код:
30,5 млрд параметров × 2 байта ≈ 61 ГБ
С учётом активаций и служебной памяти одной видеокарты на 80 ГБ может оказаться недостаточно для длинного контекста.
## QLoRA
При QLoRA базовая модель загружается в 4-битном виде.
Обучаемые LoRA-матрицы при этом остаются в более высокой точности.
Преимущества:
- значительно меньше расход видеопамяти;
- можно работать с моделью на более доступном оборудовании;
- размер адаптера остаётся небольшим.
Недостатки:
- обучение может быть медленнее;
- для MoE-моделей возможны дополнительные ограничения;
- 24 ГБ видеопамяти всё равно могут оказаться недостаточными;
- результат зависит от реализации квантования и версий библиотек.
| Видеопамять | Практический вариант |
|---:|---|
| 24 ГБ | Экспериментальная QLoRA, контекст 512–1024, возможен OOM |
| 32 ГБ | QLoRA с небольшим контекстом |
| 40–48 ГБ | Рекомендуемый минимум для удобной QLoRA |
| 80 ГБ | QLoRA с более длинными примерами или короткая BF16 LoRA |
| Несколько GPU | BF16 LoRA, FSDP или DeepSpeed |
Точные значения зависят от:
- длины последовательности;
- версии PyTorch;
- реализации MoE;
- набора LoRA-модулей;
- размера batch;
- gradient checkpointing;
- используемого оптимизатора;
- модели видеокарты.
# Требования к компьютеру
Рекомендуемая конфигурация:
Код:
ОС: Ubuntu 22.04/24.04 или Debian 12
GPU: NVIDIA с поддержкой CUDA
VRAM: желательно 40–48 ГБ
RAM: минимум 64 ГБ
Диск: минимум 120 ГБ свободного места
Python: 3.10 или 3.11
Файловая система: SSD или NVMe
Для RTX 3090 или RTX 4090 с 24 ГБ:
Код:
max_length: 512–1024
per_device_train_batch_size: 1
gradient_accumulation_steps: 16–32
LoRA rank: 8
LoRA target: только attention
Даже с такими настройками обучение не гарантированно поместится в 24 ГБ.
---
# Что получится после обучения
LoRA-адаптер обычно содержит:
Код:
adapter_config.json
adapter_model.safetensors
tokenizer_config.json
training_args.bin
Размер адаптера может составлять от десятков мегабайт до нескольких гигабайт — в зависимости от:
- ранга LoRA;
- количества целевых модулей;
- количества слоёв;
- обучения embedding;
- особенностей MoE.
Для запуска потребуются:
Код:
Базовая Qwen3-Coder
+
LoRA-адаптер
Альтернативный вариант — объединить адаптер с базовой моделью.
---
# Подготовка обучающих данных
Качество датасета важнее его размера.
Плохие 100 000 примеров могут ухудшить модель сильнее, чем 2 000 качественных примеров.
## Хороший пример должен быть
- технически правильным;
- проверяемым;
- однозначным;
- полным;
- написанным в едином стиле;
- без секретов;
- без случайных обрывков;
- без лишних повторов;
- без неверного кода.
- пароли;
- API-ключи;
- приватные ключи;
- seed-фразы;
- токены;
- cookies;
- персональные данные;
- конфигурации с реальными секретами;
- чужой код без разрешения;
- исходники с несовместимой лицензией;
- уязвимый код без объяснения, что он уязвим;
- ответы, созданные слабой моделью без проверки.
Bash:
grep -RniE \
'(password|passwd|api[_-]?key|secret|token|private[_-]?key)' \
dataset/
Это не заменяет полноценный сканер секретов, но помогает найти очевидные значения.
---
# Сколько примеров нужно
Ориентировочно:
| Цель | Количество качественных примеров |
|---|---:|
| Изменение формата ответа | 300–1 000 |
| Стиль программирования | 1 000–5 000 |
| Узкая специализированная задача | 2 000–10 000 |
| Несколько языков программирования | 10 000–50 000 |
| Сложное агентное поведение | десятки тысяч и больше |
Один пример может включать несколько сообщений.
Важно не количество строк, а покрытие реальных сценариев.
---
# Формат датасета
Будем использовать формат JSON Lines.
Каждая строка — отдельный JSON-объект:
JSON:
{"messages":[{"role":"system","content":"Ты помощник программиста."},{"role":"user","content":"Напиши функцию сложения."},{"role":"assistant","content":"```python\ndef add(a: int, b: int) -> int:\n return a + b\n```"}]}
Создадим каталог проекта:
Bash:
mkdir -p ~/qwen3-coder-lora/data
cd ~/qwen3-coder-lora
Создайте файл:
Bash:
nano data/train.jsonl
Пример содержимого:
JSON:
{"messages":[{"role":"system","content":"Ты опытный Python-разработчик. Пиши безопасный, типизированный и понятный код."},{"role":"user","content":"Напиши функцию чтения JSON-файла. Добавь обработку ошибок."},{"role":"assistant","content":"```python\nfrom __future__ import annotations\n\nimport json\nfrom pathlib import Path\nfrom typing import Any\n\n\ndef load_json(path: str | Path) -> dict[str, Any]:\n file_path = Path(path)\n\n try:\n with file_path.open(\"r\", encoding=\"utf-8\") as file:\n data = json.load(file)\n except FileNotFoundError as exc:\n raise RuntimeError(f\"Файл не найден: {file_path}\") from exc\n except PermissionError as exc:\n raise RuntimeError(f\"Нет доступа к файлу: {file_path}\") from exc\n except json.JSONDecodeError as exc:\n raise RuntimeError(f\"Некорректный JSON: {exc}\") from exc\n\n if not isinstance(data, dict):\n raise RuntimeError(\"Корневой элемент JSON должен быть объектом\")\n\n return data\n```\n\nФункция явно обрабатывает отсутствие файла, ошибки доступа и некорректный JSON."}]}
{"messages":[{"role":"system","content":"Ты выполняешь ревью кода. Сначала укажи проблему, затем предложи исправление."},{"role":"user","content":"Проверь код:\n\n```python\ndef divide(a, b):\n return a / b\n```"},{"role":"assistant","content":"В функции отсутствуют аннотации типов и явная обработка деления на ноль.\n\n```python\ndef divide(a: float, b: float) -> float:\n if b == 0:\n raise ValueError(\"Делитель не должен быть равен нулю\")\n\n return a / b\n```\n\nИсключение `ValueError` сообщает вызывающему коду, что передан недопустимый аргумент."}]}
{"messages":[{"role":"system","content":"Ты системный программист. Не изменяй поведение программы без необходимости."},{"role":"user","content":"Исправь утечку памяти в этом коде:\n\n```c\nchar *buffer = malloc(1024);\nif (!buffer) return -1;\nif (read_data(buffer) < 0) return -2;\nfree(buffer);\nreturn 0;\n```"},{"role":"assistant","content":"При ошибке `read_data()` функция завершается до вызова `free()`.\n\nМинимальное исправление:\n\n```c\nchar *buffer = malloc(1024);\nif (!buffer) {\n return -1;\n}\n\nif (read_data(buffer) < 0) {\n free(buffer);\n return -2;\n}\n\nfree(buffer);\nreturn 0;\n```\n\nДля более сложной функции можно использовать единую секцию очистки через `goto cleanup`, чтобы не дублировать освобождение ресурсов."}]}
Каждая строка файла должна быть самостоятельным корректным JSON.
---
# Системные сообщения
Системное сообщение определяет желаемое поведение.
Примеры:
Код:
Ты выполняешь минимальные исправления и не переписываешь
весь файл без необходимости.
Код:
Всегда объясняй причину ошибки перед исправлением.
Код:
Используй Python 3.12, аннотации типов и pathlib.
Код:
Не предлагай небезопасные способы хранения паролей.
Не следует использовать слишком длинное системное сообщение в каждом примере.
Можно определить общий стиль коротко и последовательно.
---
# Разделение на train и validation
Нельзя оценивать модель только на тех примерах, на которых она обучалась.
Обычно данные делят:
Код:
90–95% — обучение
5–10% — проверка
Для небольшого набора можно использовать:
Код:
95% train
5% validation
Кроме validation-набора подготовьте отдельный набор реальных задач, которые модель никогда не видела.
---
# Установка окружения
## 1. Проверка драйвера NVIDIA
Bash:
nvidia-smi
Команда должна показать:
- модель GPU;
- версию драйвера;
- объём памяти;
- запущенные процессы.
## 2. Установка системных пакетов
Ubuntu или Debian:
Bash:
sudo apt update
sudo apt install -y \
git \
python3 \
python3-venv \
python3-pip \
build-essential \
pkg-config
## 3. Создание виртуального окружения
Bash:
cd ~/qwen3-coder-lora
python3 -m venv .venv
source .venv/bin/activate
Обновите инструменты Python:
Bash:
python -m pip install --upgrade \
pip \
setuptools \
wheel
## 4. Установка библиотек
Bash:
pip install --upgrade \
torch \
transformers \
datasets \
accelerate \
peft \
trl \
bitsandbytes \
safetensors \
sentencepiece
После установки сохраните версии:
Bash:
pip freeze > requirements-lock.txt
Проверка:
Bash:
python -c \
'import torch, transformers, peft, trl; \
print("PyTorch:", torch.__version__); \
print("Transformers:", transformers.__version__); \
print("PEFT:", peft.__version__); \
print("TRL:", trl.__version__); \
print("CUDA:", torch.cuda.is_available())'
Ожидаемый результат:
Код:
CUDA: True
Qwen3-MoE требует современной версии Transformers. При слишком старой версии может появиться:
Код:
KeyError: 'qwen3_moe'
В таком случае обновите:
Bash:
pip install --upgrade transformers
---
# Проверка датасета
Создайте файл:
Bash:
nano validate_dataset.py
Содержимое:
Python:
from __future__ import annotations
import json
import sys
from pathlib import Path
DATASET_PATH = Path("data/train.jsonl")
ALLOWED_ROLES = {"system", "user", "assistant", "tool"}
def fail(line_number: int, message: str) -> None:
print(f"Ошибка в строке {line_number}: {message}", file=sys.stderr)
raise SystemExit(1)
def main() -> None:
if not DATASET_PATH.is_file():
raise SystemExit(f"Файл не найден: {DATASET_PATH}")
examples = 0
assistants = 0
with DATASET_PATH.open("r", encoding="utf-8") as file:
for line_number, raw_line in enumerate(file, start=1):
line = raw_line.strip()
if not line:
continue
try:
item = json.loads(line)
except json.JSONDecodeError as exc:
fail(line_number, f"некорректный JSON: {exc}")
messages = item.get("messages")
if not isinstance(messages, list) or not messages:
fail(line_number, "поле messages должно быть непустым списком")
for message_number, message in enumerate(messages, start=1):
if not isinstance(message, dict):
fail(
line_number,
f"сообщение {message_number} не является объектом",
)
role = message.get("role")
content = message.get("content")
if role not in ALLOWED_ROLES:
fail(
line_number,
f"неподдерживаемая роль: {role!r}",
)
if not isinstance(content, str) or not content.strip():
fail(
line_number,
f"пустой content в сообщении {message_number}",
)
if role == "assistant":
assistants += 1
if not any(message.get("role") == "assistant" for message in messages):
fail(line_number, "пример не содержит ответа assistant")
examples += 1
print(f"Примеров: {examples}")
print(f"Ответов assistant: {assistants}")
print("Датасет прошёл базовую проверку.")
if __name__ == "__main__":
main()
Запуск:
Bash:
python validate_dataset.py
---
# Скрипт обучения QLoRA
Создайте файл:
Bash:
nano train_qwen3_coder_qlora.py
Содержимое:
Python:
from __future__ import annotations
import os
from pathlib import Path
import torch
from datasets import load_dataset
from peft import LoraConfig
from transformers import AutoTokenizer, BitsAndBytesConfig
from trl import SFTConfig, SFTTrainer
MODEL_ID = os.environ.get(
"MODEL_ID",
"Qwen/Qwen3-Coder-30B-A3B-Instruct",
)
DATASET_PATH = os.environ.get(
"DATASET_PATH",
"data/train.jsonl",
)
OUTPUT_DIR = os.environ.get(
"OUTPUT_DIR",
"output/qwen3-coder-lora",
)
MAX_LENGTH = int(os.environ.get("MAX_LENGTH", "2048"))
LORA_RANK = int(os.environ.get("LORA_RANK", "16"))
EPOCHS = float(os.environ.get("EPOCHS", "2"))
def main() -> None:
if not torch.cuda.is_available():
raise RuntimeError(
"CUDA не обнаружена. Проверьте драйвер NVIDIA "
"и CUDA-версию PyTorch."
)
if not Path(DATASET_PATH).is_file():
raise FileNotFoundError(f"Датасет не найден: {DATASET_PATH}")
use_bf16 = torch.cuda.is_bf16_supported()
compute_dtype = torch.bfloat16 if use_bf16 else torch.float16
print(f"Модель: {MODEL_ID}")
print(f"Датасет: {DATASET_PATH}")
print(f"Результат: {OUTPUT_DIR}")
print(f"MAX_LENGTH: {MAX_LENGTH}")
print(f"LoRA rank: {LORA_RANK}")
print(f"BF16: {use_bf16}")
tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
use_fast=True,
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
dataset = load_dataset(
"json",
data_files=DATASET_PATH,
split="train",
)
if len(dataset) < 20:
raise RuntimeError(
"Для разделения train/validation желательно "
"иметь не менее 20 примеров."
)
split = dataset.train_test_split(
test_size=0.05,
seed=42,
)
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=compute_dtype,
)
# Этот вариант обучает только attention-проекции.
# Он требует меньше памяти и обычно стабильнее для первого запуска.
lora_config = LoraConfig(
task_type="CAUSAL_LM",
r=LORA_RANK,
lora_alpha=LORA_RANK * 2,
lora_dropout=0.05,
bias="none",
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
],
use_rslora=True,
)
training_config = SFTConfig(
output_dir=OUTPUT_DIR,
num_train_epochs=EPOCHS,
per_device_train_batch_size=1,
per_device_eval_batch_size=1,
gradient_accumulation_steps=16,
learning_rate=1.0e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
weight_decay=0.01,
max_grad_norm=1.0,
max_length=MAX_LENGTH,
# Начинать лучше без packing.
# После стабильного запуска его можно протестировать отдельно.
packing=False,
# Loss считается только на ответах assistant.
assistant_only_loss=True,
gradient_checkpointing=True,
gradient_checkpointing_kwargs={
"use_reentrant": False,
},
use_cache=False,
# Оптимизатор bitsandbytes уменьшает расход памяти.
optim="paged_adamw_8bit",
bf16=use_bf16,
fp16=not use_bf16,
logging_strategy="steps",
logging_steps=5,
logging_first_step=True,
eval_strategy="steps",
eval_steps=100,
save_strategy="steps",
save_steps=100,
save_total_limit=2,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
report_to="none",
seed=42,
data_seed=42,
dataset_num_proc=max(
1,
min(8, (os.cpu_count() or 2) // 2),
),
)
trainer = SFTTrainer(
model=MODEL_ID,
args=training_config,
train_dataset=split["train"],
eval_dataset=split["test"],
processing_class=tokenizer,
peft_config=lora_config,
quantization_config=quantization_config,
)
trainer.model.print_trainable_parameters()
trainer.train()
final_dir = Path(OUTPUT_DIR) / "final"
trainer.save_model(str(final_dir))
tokenizer.save_pretrained(str(final_dir))
print(f"Готово. Адаптер сохранён в: {final_dir}")
if __name__ == "__main__":
main()
---
# Запуск обучения
Активируйте окружение:
Bash:
cd ~/qwen3-coder-lora
source .venv/bin/activate
Проверьте данные:
Bash:
python validate_dataset.py
Запустите обучение:
Bash:
CUDA_VISIBLE_DEVICES=0 \
python train_qwen3_coder_qlora.py
Для видеокарты с 24 ГБ начните с минимального режима:
Bash:
CUDA_VISIBLE_DEVICES=0 \
MAX_LENGTH=512 \
LORA_RANK=8 \
EPOCHS=1 \
python train_qwen3_coder_qlora.py
Если обучение помещается в память:
Bash:
CUDA_VISIBLE_DEVICES=0 \
MAX_LENGTH=1024 \
LORA_RANK=8 \
EPOCHS=2 \
python train_qwen3_coder_qlora.py
Для 48 ГБ:
Bash:
CUDA_VISIBLE_DEVICES=0 \
MAX_LENGTH=2048 \
LORA_RANK=16 \
EPOCHS=2 \
python train_qwen3_coder_qlora.py
---
# Наблюдение за видеокартой
В отдельном терминале:
Bash:
watch -n 1 nvidia-smi
Следите за:
Memory-Usage;GPU-Util;- температурой;
- энергопотреблением;
- процессом Python.
Если используется почти вся память, это нормально.
Но желательно оставить небольшой запас, чтобы случайный длинный batch не вызвал OOM.
---
# Что означают параметры LoRA
## r
Ранг адаптера:
Python:
r=16
Чем больше
r:- тем выше обучающая способность;
- тем больше размер адаптера;
- тем больше расход памяти;
- тем медленнее обучение.
Практические значения:
Код:
r=8 — минимальное обучение, экономия памяти
r=16 — хороший начальный вариант
r=32 — больше способности к адаптации
r=64 — сложная адаптация, больше памяти
Для первой попытки:
Код:
24 ГБ VRAM → r=8
48 ГБ VRAM → r=16
80 ГБ VRAM → r=16 или r=32
## lora_alpha
Масштаб LoRA-обновления:
Python:
lora_alpha=32
Часто используется соотношение:
Код:
lora_alpha = 2 × r
Это не обязательное правило, но хороший начальный вариант.
## lora_dropout
Python:
lora_dropout=0.05
Dropout уменьшает риск переобучения.
Для крупного качественного датасета можно использовать:
Код:
0.0–0.05
Для небольшого:
Код:
0.05–0.1
## target_modules
В примере обучаются:
Python:
[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
]
Это attention-проекции.
Такой режим:
- требует меньше памяти;
- проще для MoE;
- подходит для первого эксперимента.
Более широкий вариант:
Python:
target_modules="all-linear"
Он может улучшить адаптацию, но:
- требует больше памяти;
- создаёт более крупный адаптер;
- может работать медленнее;
- для MoE-экспертов может потребоваться дополнительная настройка.
Не переключайтесь на
all-linear, пока базовая конфигурация не работает стабильно.---
# Длина последовательности
Qwen3-Coder поддерживает огромный контекст, но это не означает, что модель нужно обучать на 262 144 токенах.
Расход памяти на активации быстро увеличивается с длиной.
Для начала:
Код:
512 токенов — проверка работоспособности
1024 токена — небольшие функции
2048 токенов — функции и небольшие файлы
4096 токенов — более крупные примеры
8192 токенов — требует существенно больше памяти
Не задавайте большой
MAX_LENGTH только потому, что модель его поддерживает.Сначала изучите реальные длины примеров.
---
# Проверка длины примеров
Создайте файл:
Bash:
nano inspect_lengths.py
Содержимое:
Python:
from __future__ import annotations
import statistics
from datasets import load_dataset
from transformers import AutoTokenizer
MODEL_ID = "Qwen/Qwen3-Coder-30B-A3B-Instruct"
DATASET_PATH = "data/train.jsonl"
def main() -> None:
tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
use_fast=True,
)
dataset = load_dataset(
"json",
data_files=DATASET_PATH,
split="train",
)
lengths: list[int] = []
for example in dataset:
text = tokenizer.apply_chat_template(
example["messages"],
tokenize=False,
add_generation_prompt=False,
)
tokens = tokenizer(
text,
add_special_tokens=False,
)["input_ids"]
lengths.append(len(tokens))
ordered = sorted(lengths)
def percentile(value: float) -> int:
index = min(
len(ordered) - 1,
int(len(ordered) * value),
)
return ordered[index]
print(f"Примеров: {len(lengths)}")
print(f"Минимум: {min(lengths)}")
print(f"Среднее: {statistics.mean(lengths):.1f}")
print(f"Медиана: {statistics.median(lengths):.1f}")
print(f"P90: {percentile(0.90)}")
print(f"P95: {percentile(0.95)}")
print(f"Максимум: {max(lengths)}")
if __name__ == "__main__":
main()
Запуск:
Bash:
python inspect_lengths.py
Если результат:
Код:
P95: 1800
Максимум: 9200
можно выбрать:
Код:
MAX_LENGTH=2048
а очень длинные примеры обработать отдельно.
---
# Как понять, что обучение идёт нормально
В журнале появятся значения:
Код:
loss
eval_loss
learning_rate
epoch
grad_norm
Ожидаемая тенденция:
Код:
train loss постепенно уменьшается
eval loss сначала уменьшается
Плохой признак:
Код:
train loss уменьшается
eval loss постоянно увеличивается
Это может означать переобучение.
Другие признаки проблем:
Код:
loss = nan
grad_norm = nan
eval_loss резко растёт
модель повторяет ответы
модель копирует train-примеры дословно
качество базовых задач ухудшилось
---
# Сколько эпох использовать
Начните с:
Код:
1–2 эпох
Для маленького датасета большое количество эпох быстро приводит к запоминанию.
Пример:
| Размер датасета | Начальное количество эпох |
|---:|---:|
| 500 примеров | 2–4 |
| 2 000 примеров | 2–3 |
| 10 000 примеров | 1–2 |
| 50 000 примеров | 1 |
Это только ориентир.
Решение принимается по:
- validation loss;
- качеству ответов;
- тестам кода;
- отсутствию деградации;
- сравнению checkpoint.
---
# Тестирование LoRA-адаптера
Создайте файл:
Bash:
nano test_adapter.py
Содержимое:
Python:
from __future__ import annotations
import os
import torch
from peft import PeftModel
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
MODEL_ID = "Qwen/Qwen3-Coder-30B-A3B-Instruct"
ADAPTER_PATH = os.environ.get(
"ADAPTER_PATH",
"output/qwen3-coder-lora/final",
)
def main() -> None:
use_bf16 = torch.cuda.is_bf16_supported()
compute_dtype = torch.bfloat16 if use_bf16 else torch.float16
quantization_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=compute_dtype,
)
tokenizer = AutoTokenizer.from_pretrained(
ADAPTER_PATH,
use_fast=True,
)
base_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=quantization_config,
device_map="auto",
torch_dtype=compute_dtype,
)
model = PeftModel.from_pretrained(
base_model,
ADAPTER_PATH,
)
model.eval()
messages = [
{
"role": "system",
"content": (
"Ты выполняешь ревью кода. "
"Сначала объясни проблему, затем предложи исправление."
),
},
{
"role": "user",
"content": """
Проверь код:
```python
def read_file(path):
file = open(path)
return file.read()
},
]
inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
return_tensors="pt",
return_dict=True,
).to(model.device)
with torch.inference_mode():
output = model.generate(
**inputs,
max_new_tokens=512,
do_sample=False,
repetition_penalty=1.05,
)
generated = output[0, inputs["input_ids"].shape[1]:]
print(
tokenizer.decode(
generated,
skip_special_tokens=True,
)
)
if name == "main":
main()
Код:
Запуск:
```bash
ADAPTER_PATH=output/qwen3-coder-lora/final \
python test_adapter.py
---
# Сравнение с базовой моделью
Обязательно сравните:
Код:
Базовая Qwen3-Coder
против
Qwen3-Coder + LoRA
Используйте одинаковые:
- системные инструкции;
- prompts;
- параметры генерации;
- тестовые задачи;
- ограничения времени.
---
# Автоматическое тестирование кода
Для Python можно проверять:
Bash:
python -m compileall generated/
Стиль:
Bash:
ruff check generated/
Типы:
Bash:
mypy generated/
Тесты:
Bash:
pytest -q
Для C и C++:
Bash:
gcc -Wall -Wextra -Werror source.c -o program
Для JavaScript:
Bash:
npm test
Для Go:
Bash:
go test ./...
Оценивать нужно:
- компилируется ли код;
- проходят ли тесты;
- соответствует ли решение задаче;
- нет ли уязвимостей;
- сохраняется ли API;
- не удалены ли важные проверки.
---
# Не оценивайте модель только по loss
Низкий loss не гарантирует хороший код.
Модель может:
- заучить датасет;
- писать синтаксически правильный, но неверный код;
- менять поведение программы;
- создавать уязвимости;
- придумывать библиотеки;
- удалять обработку ошибок;
- чрезмерно переписывать файлы.
Например:
Код:
1. Исправление утечки памяти
2. Обработка исключения
3. Рефакторинг без изменения API
4. Поиск race condition
5. Проверка SQL-запроса
6. Добавление unit-тестов
7. Исправление Dockerfile
8. Анализ Nginx-конфигурации
9. Поиск логической ошибки
10. Объяснение незнакомого кода
---
# Возобновление обучения
Во время обучения создаются каталоги:
Код:
checkpoint-100
checkpoint-200
checkpoint-300
Для продолжения измените строку:
Python:
trainer.train()
на:
Python:
trainer.train(
resume_from_checkpoint=True,
)
Или укажите точный путь:
Python:
trainer.train(
resume_from_checkpoint=(
"output/qwen3-coder-lora/checkpoint-300"
),
)
Не продолжайте обучение с checkpoint, созданного при несовместимых:
- версиях библиотек;
- настройках LoRA;
- tokenizer;
- параметрах модели;
- форматах датасета.
# Объединение LoRA с базовой моделью
Объединение создаёт обычную модель, в веса которой внесены LoRA-изменения.
Для этого требуется загрузить базовую модель в BF16 или FP16.
Понадобится много оперативной памяти:
Код:
желательно 80–96 ГБ RAM
Создайте файл:
Bash:
nano merge_adapter.py
Содержимое:
Python:
from __future__ import annotations
import os
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_ID = "Qwen/Qwen3-Coder-30B-A3B-Instruct"
ADAPTER_PATH = os.environ.get(
"ADAPTER_PATH",
"output/qwen3-coder-lora/final",
)
MERGED_PATH = os.environ.get(
"MERGED_PATH",
"output/qwen3-coder-merged",
)
def main() -> None:
tokenizer = AutoTokenizer.from_pretrained(
ADAPTER_PATH,
use_fast=True,
)
base_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
torch_dtype=torch.bfloat16,
device_map="cpu",
low_cpu_mem_usage=True,
)
model = PeftModel.from_pretrained(
base_model,
ADAPTER_PATH,
)
merged_model = model.merge_and_unload()
merged_model.save_pretrained(
MERGED_PATH,
safe_serialization=True,
max_shard_size="4GB",
)
tokenizer.save_pretrained(MERGED_PATH)
print(f"Объединённая модель: {MERGED_PATH}")
if __name__ == "__main__":
main()
Запуск:
Bash:
ADAPTER_PATH=output/qwen3-coder-lora/final \
MERGED_PATH=output/qwen3-coder-merged \
python merge_adapter.py
Не объединяйте адаптер с уже квантованной 4-битной моделью для получения основной финальной копии. Сначала загрузите исходную модель в BF16 или FP16.
---
# Запуск без объединения
Объединять модель необязательно.
Преимущества отдельного адаптера:
- занимает меньше места;
- можно быстро менять адаптеры;
- базовая модель хранится один раз;
- удобно сравнивать версии;
- проще продолжать обучение.
Схема:
Код:
Qwen3-Coder base
├── security-lora
├── python-lora
├── c-review-lora
└── zer0kernel-style-lora
---
# Подключение к Open WebUI
Open WebUI не обучает LoRA самостоятельно.
Для подключения необходимо запустить модель через сервер с OpenAI-совместимым API.
Варианты:
- vLLM;
- SGLang;
- собственный FastAPI-сервер;
- другой совместимый inference backend.
Для production-развёртывания обычно удобнее:
Код:
1. Объединить LoRA с базовой моделью
2. При необходимости квантовать результат
3. Запустить через vLLM или SGLang
4. Подключить URL API к Open WebUI
Пример для сервера с достаточным объёмом GPU:
Bash:
vllm serve \
./output/qwen3-coder-merged \
--host 0.0.0.0 \
--port 8000 \
--max-model-len 32768
Адрес API:
Код:
http://IP-СЕРВЕРА:8000/v1
Для одной видеокарты на 24 ГБ объединённая BF16-модель не поместится.
Потребуется:
- повторное квантование;
- распределение на несколько GPU;
- CPU offload;
- либо запуск базовой 4-битной модели с адаптером через Transformers.
# Обучение использованию инструментов
Qwen3-Coder может использовать function calling.
Чтобы адаптировать это поведение, в датасете должны присутствовать:
- список доступных инструментов;
- запрос пользователя;
- вызов инструмента;
- ответ инструмента;
- финальный ответ.
Упрощённый пример:
JSON:
{
"messages": [
{
"role": "user",
"content": "Покажи последние ошибки Nginx"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"type": "function",
"function": {
"name": "read_nginx_errors",
"arguments": "{\"lines\":100}"
}
}
]
},
{
"role": "tool",
"content": "2026/07/20 upstream timed out..."
},
{
"role": "assistant",
"content": "В последних строках обнаружен таймаут upstream..."
}
]
}
Не пытайтесь обучить tool calling несколькими случайными примерами.
Необходимо обеспечить:
- единый формат схем;
- корректные аргументы;
- реальные результаты инструментов;
- правильные ответы после вызова;
- примеры ошибок инструмента;
- примеры отказа от ненужного вызова.
# Настройка под собственный репозиторий
Не превращайте каждый исходный файл в один огромный ответ.
Лучше формировать задачи:
Код:
Вот функция и тесты.
Найди ошибку и исправь её.
Код:
Вот интерфейс и реализация.
Проверь соответствие.
Код:
Вот diff.
Выполни code review.
Код:
Вот лог ошибки и связанные функции.
Определи первичную причину.
Код:
Вот старый код и требования.
Внеси минимальные изменения.
Особенно полезны данные из истории реальной разработки:
Код:
Issue
+
Исходное состояние кода
+
Исправленный commit
+
Объяснение причины
Но перед использованием необходимо проверить:
- права на код;
- лицензии;
- наличие секретов;
- персональные данные;
- внутреннюю информацию;
- качество исправления.
# Почему модель может стать хуже
Причины:
- слишком высокий learning rate;
- много эпох;
- однообразный датасет;
- ошибочные ответы;
- слишком маленький набор данных;
- отсутствие validation;
- дублирование примеров;
- обучение только одному языку;
- противоречивые системные инструкции;
- обрезание важных частей;
- train и test содержат одинаковые задачи.
- модель отвечает одинаковым шаблоном;
- перестаёт выполнять общие задачи;
- чрезмерно использует один язык;
- вставляет лишние объяснения;
- повторяет системное сообщение;
- создаёт код из обучающих примеров дословно;
- игнорирует инструкции пользователя.
---
# Что делать при CUDA Out of Memory
Ошибка:
Код:
torch.OutOfMemoryError: CUDA out of memory
Попробуйте по порядку.
## 1. Уменьшить длину
Bash:
MAX_LENGTH=1024
или:
Bash:
MAX_LENGTH=512
## 2. Уменьшить ранг
Bash:
LORA_RANK=8
## 3. Оставить batch равным 1
Python:
per_device_train_batch_size=1
## 4. Не включать packing
Python:
packing=False
## 5. Закрыть другие GPU-процессы
Bash:
nvidia-smi
## 6. Уменьшить фрагментацию памяти
Bash:
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
Затем:
Bash:
MAX_LENGTH=512 \
LORA_RANK=8 \
python train_qwen3_coder_qlora.py
## 7. Использовать более мощную GPU
Для Qwen3-Coder-30B это нередко единственное практическое решение.
Видеокарта с 24 ГБ находится на нижней границе возможностей и может не поддерживать обучение конкретной версии модели даже при минимальных настройках.
---
# Ошибка KeyError: qwen3_moe
Ошибка:
Код:
KeyError: 'qwen3_moe'
Причина — старая версия Transformers.
Исправление:
Bash:
pip install --upgrade \
transformers \
accelerate \
peft \
trl
После обновления перезапустите Python-процесс.
---
# Ошибка assistant_only_loss
Возможная ошибка связана с отсутствием специальных generation-маркеров в chat template.
Сначала обновите TRL:
Bash:
pip install --upgrade trl transformers
Если ошибка сохраняется, временно замените:
Python:
assistant_only_loss=True
на:
Python:
assistant_only_loss=False
Но в таком режиме loss будет рассчитываться и на системных и пользовательских сообщениях.
Предпочтительнее обновить библиотеки и использовать совместимый шаблон.
---
# Loss становится NaN
Попробуйте:
1. Уменьшить learning rate:
Python:
learning_rate=5.0e-5
2. Проверить пустые ответы.
3. Проверить чрезмерно длинные примеры.
4. Проверить BF16/FP16.
5. Отключить повреждённый пример.
6. Проверить датасет на некорректные значения.
7. Уменьшить LoRA rank.
8. Проверить версию bitsandbytes.
---
# Рекомендуемые стартовые параметры
## Минимальная память
YAML:
max_length: 512
batch_size: 1
gradient_accumulation: 32
lora_rank: 8
lora_alpha: 16
learning_rate: 0.0001
epochs: 1
target: attention
## Сбалансированный режим
YAML:
max_length: 2048
batch_size: 1
gradient_accumulation: 16
lora_rank: 16
lora_alpha: 32
learning_rate: 0.0001
epochs: 2
target: attention
## Более глубокая адаптация
YAML:
max_length: 4096
batch_size: 1
gradient_accumulation: 16
lora_rank: 32
lora_alpha: 64
learning_rate: 0.00005
epochs: 1–2
target: all-linear
Последний вариант требует существенно больше памяти.
---
# Альтернатива: LLaMA-Factory
Если не хочется писать Python-код, можно использовать LLaMA-Factory.
Установка:
Bash:
git clone --depth 1 \
https://github.com/hiyouga/LLaMA-Factory.git
cd LLaMA-Factory
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -e .
Проверка:
Bash:
llamafactory-cli version
Запуск WebUI:
Bash:
llamafactory-cli webui
В интерфейсе необходимо выбрать:
Код:
Model:
Qwen/Qwen3-Coder-30B-A3B-Instruct
Stage:
Supervised Fine-Tuning
Fine-tuning method:
LoRA
Quantization:
4 bit
Template:
Qwen3 без thinking-режима
Batch size:
1
Gradient accumulation:
16
Learning rate:
0.0001
Maximum sequence length:
512–2048
Название шаблона может меняться между версиями LLaMA-Factory. Используйте шаблон Qwen3, предназначенный для non-thinking/Instruct-моделей.
---
# Безопасность обучения
Не запускайте неизвестные обучающие скрипты от root.
Не передавайте:
- Hugging Face token в открытом коде;
- приватные репозитории без проверки;
- API-ключи в датасете;
- Docker socket обучающему контейнеру;
- каталог
/root; - SSH-ключи;
- конфигурации production-сервера.
Bash:
huggingface-cli login
или переменную окружения:
Bash:
export HF_TOKEN="..."
Не добавляйте токен в:
Код:
train.jsonl
train.py
docker-compose.yml
Git-репозиторий
---
# Публикация адаптера
Перед публикацией проверьте:
- лицензию базовой модели;
- лицензии датасета;
- наличие приватного кода;
- наличие персональных данных;
- наличие секретов;
- возможное запоминание примеров;
- назначение адаптера;
- ограничения использования.
Код:
Base model
Тип обучения
Размер датасета
Языки программирования
LoRA rank
Learning rate
Количество эпох
Максимальную длину
Ограничения
Результаты тестирования
Пример:
Markdown (GitHub flavored):
## Base model
Qwen/Qwen3-Coder-30B-A3B-Instruct
## Training
- Method: QLoRA SFT
- LoRA rank: 16
- LoRA alpha: 32
- Max length: 2048
- Epochs: 2
- Learning rate: 1e-4
## Dataset
4 500 проверенных примеров ревью Python и C-кода.
## Limitations
Адаптер не предназначен для генерации production-кода
без проверки разработчиком.
---
# Чек-лист перед обучением
Код:
[ ] Выбрана правильная базовая модель.
[ ] Понятно, почему используется LoRA, а не RAG.
[ ] Код разрешено использовать для обучения.
[ ] Из данных удалены секреты.
[ ] Удалены дубликаты.
[ ] Ответы проверены специалистом.
[ ] Создан отдельный validation-набор.
[ ] Создан набор реальных тестовых задач.
[ ] Установлены современные версии Transformers, PEFT и TRL.
[ ] CUDA определяется PyTorch.
[ ] На диске достаточно свободного места.
[ ] Начальное MAX_LENGTH не завышено.
[ ] Batch size равен 1.
[ ] Включён gradient checkpointing.
[ ] Настроено сохранение checkpoint.
[ ] Результат сравнивается с базовой моделью.
---
# Часто задаваемые вопросы
## Можно ли обучить Qwen3-Coder на RTX 3090 с 24 ГБ?
Иногда возможно запустить ограниченную QLoRA-конфигурацию:
Код:
4-bit
MAX_LENGTH=512
batch=1
LoRA rank=8
attention only
Но это не гарантируется.
Qwen3-Coder является MoE-моделью с общим размером около 30,5 млрд параметров. В зависимости от реализации квантования обучение может потребовать более 24 ГБ.
Практичнее использовать GPU с 40–48 ГБ.
## Почему активных параметров 3,3 млрд, а памяти требуется намного больше?
Активные параметры определяют, какая часть экспертов участвует в обработке конкретного токена.
Но веса всех экспертов должны храниться в памяти или быть доступны системе.
## Можно ли обучить модель на сырых исходниках?
Можно использовать continued pretraining, но это другая задача.
Для улучшения ответов на инструкции эффективнее подготовить:
Код:
задача → качественное решение
а не просто скопировать репозиторий в текстовый файл.
## Можно ли использовать только 100 примеров?
Технически можно.
Но велик риск:
- переобучения;
- запоминания;
- нестабильного поведения;
- отсутствия переноса на новые задачи.
## Заменяет ли LoRA системный prompt?
Нет.
LoRA изменяет поведение модели, но системная инструкция всё равно полезна.
Лучший результат:
Код:
хорошая базовая модель
+
качественный LoRA-адаптер
+
понятный системный prompt
+
RAG при необходимости
## Нужно ли объединять LoRA?
Нет.
Отдельный адаптер удобнее для:
- экспериментов;
- переключения задач;
- экономии диска;
- продолжения обучения.
## Можно ли обучить модель писать вредоносный код?
Технически дообучение меняет поведение модели, но использовать его следует только для законных и разрешённых задач.
Для исследований информационной безопасности лучше обучать модель:
- безопасному анализу;
- обнаружению уязвимостей;
- исправлению ошибок;
- анализу журналов;
- написанию детектов;
- защите инфраструктуры.
Зависит от:
- GPU;
- длины последовательности;
- количества примеров;
- rank;
- batch;
- числа эпох;
- скорости диска;
- реализации MoE.
Перед большим запуском выполните тест на 20–50 шагах и измерьте реальную скорость.
---
# Итоги
Для дообучения Qwen3-Coder не требуется изменять все 30,5 млрд параметров.
LoRA обучает небольшой адаптер, а QLoRA дополнительно загружает базовую модель в 4-битном виде.
Общий процесс:
Код:
1. Определить задачу.
2. Решить, нужен LoRA или RAG.
3. Подготовить качественные примеры.
4. Удалить секреты.
5. Разделить train и validation.
6. Установить Transformers, PEFT и TRL.
7. Загрузить Qwen3-Coder в 4-битном виде.
8. Добавить LoRA к attention-слоям.
9. Начать с небольшого MAX_LENGTH.
10. Следить за train и validation loss.
11. Проверять код компиляторами и тестами.
12. Сравнить адаптер с базовой моделью.
13. Сохранить отдельный адаптер.
14. При необходимости объединить модель.
15. Развернуть её через совместимый API.
Главный принцип:
Качество LoRA определяется прежде всего качеством обучающих примеров, а не количеством эпох.
Хороший адаптер должен не просто снижать loss, а устойчиво решать новые задачи лучше базовой модели, не ухудшая её общие способности.
````
