Fine-tune QLoRA на 1 GPU

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Fine-tune QLoRA на 1 GPU

1. Цель задачи

Научиться эффективно fine-tune'ить большие языковые модели (70B параметров) на одном GPU с помощью QLoRA (Quantized Low-Rank Adaptation). Используя 4-битную квантизацию и LoRA-адаптеры, вы сможете дообучить модель 70B на 500 примерах, потребляя менее 48 ГБ видеопамяти. Задача демонстрирует практические навыки работы с bitsandbytes, PEFT, Hugging Face Transformers и техниками снижения потребления памяти.

Ключевой результат Рабочий checkpoint адаптированной модели 70B (LoRA-адаптеры + базовая модель в 4-bit), обученной на 500 примерах, с измеренным loss < 1.5 на валидации.

2. Исходные данные

Что нужноОткуда взять
Базовая модель 70B (например, LLaMA-2-70B или Mistral-70B)Hugging Face Model Hub (с доступом по лицензии)
Датасет из 500 примеров (инструкции + ответы)Сгенерировать самостоятельно или взять из HF (databricks/databricks-dolly-15k, первые 500 записей)
GPU с ≥ 24 ГБ VRAM (например, A10, A100, RTX 4090)Локально, облако (Colab Pro, RunPod, Vast.ai) или симуляция на CPU (только для отладки)
Python 3.10+ и базовая средаУстановка через conda / pip

Если нет реального GPU — симулируем:

  1. Используйте ту же конфигурацию в Google Colab (бесплатный T4 — 16 ГБ не хватит для 70B, но подойдёт для 7B/8B). Для 70B нужен A100 или подобный — арендуйте через RunPod ($0.50/ч).
  2. Если бюджет ограничен, замените модель на meta-llama/Llama-3.1-8B и отработайте все шаги с той же конфигурацией QLoRA — навыки переносятся.
  3. Скачайте и закешируйте модель локально, чтобы не тратить время на повторные загрузки.

3. Технологический стек

КомпонентИнструментыНазначение
Base modelHugging Face Transformers, AutoModelForCausalLMЗагрузка и квантизация модели 70B
4-bit quantizationbitsandbytes (>=0.43.0)Сжатие весов до 4 бит (NF4)
LoRA / QLoRApeft (>=0.12.0)Создание и обучение низкоранговых адаптеров
Training looptransformers.Trainer + trl.SFTTrainerОбучение с оптимизацией памяти (gradient checkpointing)
Datasetdatasets (Hugging Face)Загрузка и токенизация 500 примеров
Управление памятьюaccelerate + deepspeed (опционально)Распределение, offload
Мониторингwandb или tensorboardЛогирование loss, градиентов, использования памяти
СистемаCUDA 12.x, PyTorch 2.4+, LinuxСреда выполнения

4. Этапы выполнения

Этап 1: Подготовка окружения и данных (30 минут)

Действия

  1. Создайте виртуальное окружение и установите зависимости:

    conda create -n qlora70b python=3.10 -y
    conda activate qlora70b
    pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
    pip install transformers accelerate bitsandbytes peft trl datasets wandb
    
  2. Получите доступ к модели 70B (если закрытая):

    • Запросите доступ на Hugging Face для meta-llama/Llama-2-70b-hf.
    • Авторизуйтесь: huggingface-cli login.
  3. Подготовьте датасет (500 примеров):

    • Загрузите Dolly-15k и выберите первые 500:
      from datasets import load_dataset
      ds = load_dataset("databricks/databricks-dolly-15k", split="train")
      ds = ds.select(range(500))
      ds = ds.train_test_split(test_size=0.1, seed=42)
      # Формат: {"instruction": str, "context": str, "response": str}
      
    • Конвертируйте в формат для causal LM (instruction -> response):
      def format_example(example):
          return {"text": f"### Instruction:\n{example['instruction']}\n\n### Response:\n{example['response']}"}
      ds = ds.map(format_example)
      
  4. Проверьте, сколько занимает модель в 4-bit:

    • 70B * 0.5 bytes/param (NF4) ≈ 35 ГБ плюс overhead — ожидайте 40-45 ГБ VRAM. Убедитесь, что GPU имеет ≥ 48 ГБ.

Ожидаемый результат этапа Среда установлена, модель загружена (хотя бы проверка доступа), датасет готов и разбит на train/val.

Этап 2: Загрузка модели и настройка QLoRA (1 час)

Действия

  1. Загрузите модель в 4-bit с помощью bitsandbytes:

    import torch
    from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
    
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_use_double_quant=True,   # Double Quantization
        bnb_4bit_compute_dtype=torch.bfloat16  # или float16, bfloat16 стабильнее
    )
    
    model = AutoModelForCausalLM.from_pretrained(
        "meta-llama/Llama-2-70b-hf",
        quantization_config=bnb_config,
        device_map="auto",
        torch_dtype=torch.bfloat16,
        trust_remote_code=True
    )
    tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-70b-hf")
    tokenizer.pad_token = tokenizer.eos_token
    
  2. Настройте LoRA-адаптеры (PEFT):

    from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
    
    model = prepare_model_for_kbit_training(model)
    
    lora_config = LoraConfig(
        r=8,                # ранг адаптера
        lora_alpha=32,
        target_modules=["q_proj", "v_proj", "k_proj", "o_proj", "gate_proj", "up_proj", "down_proj"],
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM"
    )
    
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()  # должно быть ~0.1% от всех параметров (~70M из 70B)
    
  3. Проверьте потребление памяти

    • torch.cuda.memory_summary() до и после загрузки.
    • Ожидается: ~40-45 ГБ на загрузку, + буфер для градиентов (~10 ГБ) → всего ~50-55 ГБ. Если не хватает, включите gradient_checkpointing_enable().

Ожидаемый результат этапа Модель загружена в 4-bit, LoRA адаптеры добавлены, параметры trainable показаны, память в пределах доступной.

Этап 3: Конфигурация обучения и запуск (1 час)

Действия

  1. Настройте Trainer с оптимизацией памяти

    from transformers import TrainingArguments
    from trl import SFTTrainer
    
    training_args = TrainingArguments(
        output_dir="./qlora-70b-checkpoints",
        per_device_train_batch_size=2,          # маленький batch из-за 70B
        per_device_eval_batch_size=2,
        gradient_accumulation_steps=4,          # эффективный batch = 8
        learning_rate=2e-4,
        warmup_steps=10,
        max_steps=50,                           # 500 примеров, batch 8 → ~63 шага, возьмём 50
        logging_steps=5,
        eval_steps=10,
        save_strategy="steps",
        save_steps=10,
        evaluation_strategy="steps",
        fp16=False,                             # используем bf16, если поддерживается
        bf16=True,
        optim="paged_adamw_8bit",               # экономит память
        gradient_checkpointing=True,
        gradient_checkpointing_kwargs={"use_reentrant": False},
        report_to="wandb"                       # или "tensorboard"
    )
    
  2. Создайте коллатор данных и запустите обучение:

    trainer = SFTTrainer(
        model=model,
        tokenizer=tokenizer,
        args=training_args,
        train_dataset=ds["train"],
        eval_dataset=ds["test"],
        dataset_text_field="text",
        max_seq_length=512,                     # обрезаем длинные примеры до 512 токенов
        packing=False                           # для простоты
    )
    
    # Перед обучением проверьте, что trainer может разместить data в память
    trainer.train()
    
  3. Мониторинг в WandB

    • Следите за train_loss, eval_loss, grad_norm.
    • Цель: loss < 1.5 на валидации после 50 шагов.

Ожидаемый результат этапа Обучение завершено без Out-of-Memory, в папке ./qlora-70b-checkpoints сохранены чекпоинты.

Этап 4: Оценка и сохранение финальной модели (30 минут)

Действия

  1. Загрузите лучший чекпоинт и оцените на тесте:

    from peft import PeftModel
    from transformers import pipeline
    
    base_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-70b-hf",
                                                       quantization_config=bnb_config,
                                                       device_map="auto")
    model = PeftModel.from_pretrained(base_model, "./qlora-70b-checkpoints/checkpoint-50")
    pipe = pipeline("text-generation", model=model, tokenizer=tokenizer, max_new_tokens=100)
    
    # Пример запроса
    test_prompt = "### Instruction:\nExplain quantum entanglement in simple terms.\n\n### Response:\n"
    print(pipe(test_prompt)[0]["generated_text"])
    
  2. Сохраните только адаптеры (они весят ~140 МБ):

    model.save_pretrained("./qlora-70b-final-adapter")
    tokenizer.save_pretrained("./qlora-70b-final-adapter")
    
  3. Сравните loss на тесте с baseline (модель без fine-tune):

    • Если eval_loss < 1.5 — задача выполнена.

Ожидаемый результат этапа Сохранённые LoRA-адаптеры, пример генерации, лог eval_loss.

Этап 5: Документирование и анализ (30 минут)

Действия

  1. Зафиксируйте все метрики и конфигурацию создайте markdown-файл report.md с таблицами.
  2. Сравните время обучения и используемую память: запишите peak memory из nvidia-smi.
  3. Сделайте выводы как QLoRA позволила обучить 70B на одном GPU? Что можно улучшить (больше данных, больше шагов, rank адаптера).

Ожидаемый результат этапа Файл report.md с анализом, метриками, рекомендациями.

5. Критерии приемки (Definition of Done)

  • Обучение QLoRA запущено и завершено без ошибок OOM.
  • Peak GPU memory не превышает 90% от доступной (например, < 72 ГБ для A100 80GB).
  • Финальный eval loss (perplexity) ≤ 1.5 (или улучшение относительно базовой модели).
  • LoRA-адаптеры сохранены в отдельной папке (размер < 200 МБ).
  • Модель генерирует осмысленные ответы на примерах из датасета.
  • Подготовлен файл report.md с конфигурацией, метриками и наблюдениями.
  • Воспроизводимость: указаны версии пакетов (pip freeze > requirements.txt).

6. Ожидаемый результат

  • Основной артефакт папка qlora-70b-final-adapter с adapter_config.json, adapter_model.safetensors и tokenizer.
  • Содержание LoRA веса (~140 МБ) и конфигурация, которые можно загрузить на базовую 70B модель для инференса.
  • Дополнительно report.md с метриками (train loss, eval loss, память, скорость), requirements.txt, скриншот WandB.

7. Возможные сложности и их решение

СложностьРешение
CUDA Out-of-Memory при загрузке моделиУменьшить max_memory в device_map, использовать from_pretrained(..., device_map="sequential"), включить CPU offload или арендовать GPU с большей памятью (A100 80GB).
Ошибка bitsandbytes не совместим с CUDAУстановить bitsandbytes для конкретной версии CUDA (pip install bitsandbytes==0.43.2), проверить import bitsandbytes и torch.cuda.is_available().
Медленное обучение (больше 4 часов)Уменьшить max_steps до 30, увеличить batch size (если позволяет память), отключить eval на каждом шагу.
Плохая сходимость на 500 примерахИспользовать больший learning_rate (3e-4), увеличить lora_alpha (64), добавить больше шагов (100) или увеличить рангов (r=16).
Генерация повторяющихся/бессмысленных ответовУвеличить температуру при инференсе, проверить датасет на качество, увеличить max_seq_length.
Проблемы с transformers и accelerateОбновить библиотеки до последних версий, использовать pip install --upgrade transformers accelerate.

8. Бюджет времени (оценка)

ЭтапВремя
Этап 1: Подготовка окружения и данных30 мин
Этап 2: Загрузка модели и QLoRA1 ч
Этап 3: Обучение2-3 ч
Этап 4: Оценка и сохранение30 мин
Этап 5: Документирование30 мин
Итого~5-6 часов

Примечание для первого раза: Время обучения сильно зависит от GPU. На A100 80GB 50 шагов с batch 8 занимает ~40 минут. На RTX 4090 (24 ГБ) 70B не поместится — используйте 8B модель для теста.

9. Связанные вопросы из базы знаний

ВопросТема
101Как настроить bitsandbytes для 4-bit?
205Разница между LoRA и QLoRA
310Оптимизация VRAM: gradient checkpointing
415Выбор ранга LoRA для больших моделей
520Как провести fine-tune инструкционных моделей
623Настройка SFTTrainer из TRL
730Мониторинг обучения с WandB
835Устранение OOM при загрузке моделей
899Бенчмарк памяти: 4-bit vs 8-bit

10. Чек-лист самопроверки

  • Я корректно загрузил(а) модель в 4-bit с BitsAndBytesConfig.
  • Я проверил(а), что trainable параметры < 0.2% от общего числа.
  • Я включил(а) gradient checkpointing и paged optimizer.
  • Я сохранил(а) только LoRA-адаптеры, а не полную модель.
  • Я убедился(ась), что eval loss снизился относительно начального значения.