Заметка Как дообучить Qwen3-Coder с помощью LoRA и QLoRA на собственных данных

1784986536495.png


Qwen3-Coder уже умеет:
  • писать и объяснять программный код;
  • искать ошибки;
  • рефакторить проекты;
  • работать с несколькими файлами;
  • выполнять инструкции;
  • использовать инструменты;
  • анализировать большие репозитории.
Но стандартная модель ничего не знает о:
  • внутренних правилах вашего проекта;
  • принятом стиле программирования;
  • собственных библиотеках;
  • структуре приватного репозитория;
  • специальных форматах конфигурации;
  • внутренних API;
  • правилах оформления ответов;
  • типичных задачах конкретной команды.
Для адаптации модели можно использовать LoRA.

Технология называется 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 подходит, если необходимо изменить:
  • стиль ответа;
  • формат кода;
  • последовательность действий;
  • правила оформления;
  • привычный способ решения задач;
  • терминологию;
  • поведение модели;
  • устойчивый шаблон ответа.
RAG подходит, если модели необходимо предоставить:
  • документацию;
  • новые статьи;
  • внутреннюю базу знаний;
  • описание 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 ГБ видеопамяти всё равно могут оказаться недостаточными;
  • результат зависит от реализации квантования и версий библиотек.
Для обучения Qwen3-Coder-30B желательно иметь:

| Видеопамять | Практический вариант |
|---:|---|
| 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()
""".strip(),
},
]

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 не гарантирует хороший код.

Модель может:
  • заучить датасет;
  • писать синтаксически правильный, но неверный код;
  • менять поведение программы;
  • создавать уязвимости;
  • придумывать библиотеки;
  • удалять обработку ошибок;
  • чрезмерно переписывать файлы.
Создайте набор из 50–200 реальных задач.

Например:

Код:
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-сервера.
Храните токен Hugging Face через:

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 примеров?

Технически можно.

Но велик риск:
  • переобучения;
  • запоминания;
  • нестабильного поведения;
  • отсутствия переноса на новые задачи.
Такой набор подходит для проверки pipeline, но не для серьёзного адаптера.

## Заменяет ли 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, а устойчиво решать новые задачи лучше базовой модели, не ухудшая её общие способности.
````
 
Назад
Верх Низ