Привет
Если нужно, чтобы AI LLM модель выдавала актуальные для какой либо задачи данные, есть разные способы этого достичь.
Для простых задач с известными первоначальными условиями и разового применения можно вручную изменить промпт, включив в него актуальные данные.
RAG:
Если необходимо использовать динамические данные, такие как результаты поиска по задаче, поиск по странице или документу, используется RAG (Retrieval-Augmented Generation).
В простом примере данные, такие как результат запроса к базе данных или содержимое сайта, преобразуются в текст и добавляются к запросу пользователя, для этого можно использовать специализированную легковесную embedding модель, например multilingual-e5.
Если модели нужны какие либо данные в ходе генерации ответа, используется Multi-step RAG (Iterative RAG / ReAct-like):
— LLM делает предположение и формулирует «sub-question»
— Система вызывает retriever с этим подзапросом
— LLM получает новый контекст и продолжает генерацию
LoRA:
Для решения задач, в которых модель действует как помощник в какой то определенной сфере деятельности, или когда ответы модели должны основываться на большом объеме специфических данных, которые меняются редко но должны присутствовать в контексте модели для формирования ответа, RAG может не подходить по многим причинам, таким как быстродействие, плохая оптимизация, так как мы вынуждены каждый раз добавлять во входящие данные одно и то же, а размер контекста модели может банально закончиться, и этого контекста для запроса пользователя будет меньше.
Минус дообучения — Катастрофическое забывание (catastrophic forgetting)- явление, при котором модель теряет ранее усвоенные знания, когда её дообучают на новых данных, особенно если они относятся к другой задаче или распределению.
Для дообучения существуют разные подходы, например переобучение всех весов модели, Prefix-Tuning, но лучшим подходом во многих случаях может быть LoRA (Low-Rank Adaptation).
При подходе LoRA мы берем модель, создаем и обучаем адаптеры с весами аналогичным образом, каким была обучена модель, и добавляем их к модели, при этом веса модели не меняются.
Вместо того чтобы менять все веса модели (что приводит к забыванию), LoRA:
— замораживает оригинальные веса (W₀)
— добавляет обучаемые низкоранговые матрицы (ΔW = A @ B), которые адаптируют поведение модели для новой задачи
— на этапе инференса: используется W = W₀ + ΔW, но при этом W₀ остаётся нетронутым
Это позволяет сохранить исходные знания модели, а дообучение влияет только на новые компоненты (LoRA-адаптеры).
Можно включать/отключать LoRA адаптеры — легко переключаться между задачами.
Библиотеки для LoRA:
https://github.com/microsoft/LoRA
Оригинальная реализация, устаревшая, но полезна как концепт
https://github.com/huggingface/peft
Поддерживает: LoRA, QLoRA, AdaLoRA, Prefix Tuning, Prompt Tuning
https://github.com/unslothai/unsloth
Быстрая, поддерживает Flash Attention 2, TensorRT, QLoRA
и другие.
Примеры на Python:
Исходный код приимера:
https://github.com/RomanKryvolapov/LoRA_additional_training_of_model_script
Обучение будет происходить на видеокарте, так как
— peft поддерживает обучение на CPU, но оно происходит очень долго
— unsloth не поддерживает обучение на CPU
Установлен Nvidia Driver последней версии https://www.nvidia.com/en-us/drivers
Установлен CUDA Toolkit последней версии https://developer.nvidia.com/cuda-toolkit-archive
Установлен PyTorch 2.5.0, версия для Cuda 12.4 https://pytorch.org/get-started/locally
pip3 install torch==2.5.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
Установлен Unsloth, версия для Cuda 12.4 и PyTorch 2.5.0
pip install "unsloth[cu124-torch250] @ git+https://github.com/unslothai/unsloth.git"
Скачана llama.cpp https://github.com/ggml-org/llama.cpp для конвертации модели в GGUF формат на последнем шаге
gh repo clone ggml-org/llama.cpp
Модель https://huggingface.co/google/gemma-3-4b-it скачана в папку model
Версии библиотек, requirements.txt :
transformers==4.54.0 peft==0.16.0 trl==0.19.1 datasets==2.19.1 accelerate==1.9.0 scipy==1.13.1 sentencepiece==0.1.99 unsloth==2025.7.9 torch==2.5.0+cu124 bitsandbytes==0.46.1 safetensors==0.5.3
Тестовые данные, поскольку это пример, нужно убедиться в том, что данные добавились в модель. Для этого создаем gemma_lora_data.json c с содержанием- одним и тем же фактом во множестве разных формулировок.
[
{
"prompt": "Как зовут моего кота?",
"response": "Тигрёнок"
},
{
"prompt": "Какое у моего кота имя?",
"response": "Тигрёнок"
},
...
]Unsloth:
импорты:
import torch print(torch.__version__) print(torch.version.cuda) print(torch.cuda.is_available()) import os os.environ["UNSLOTH_PATCH_RL_TRAINERS"] = "false" os.environ["UNSLOTH_COMPILE_DISABLE"] = "1" os.environ["TORCHINDUCTOR_DISABLED"] = "1" import json import shutil import subprocess from pathlib import Path from datasets import Dataset from unsloth import FastLanguageModel from trl import SFTTrainer, SFTConfig
Константы, путь к скрипту GGUF_CONVERTER у вас может отличаться:
MODEL_DIR = Path("./model")
LORA_OUTPUT = Path("./lora")
MERGED_DIR = Path("./merged")
GGUF_DIR = Path("./gguf")
GGUF_PATH = GGUF_DIR / "model.gguf"
GGUF_CONVERTER = Path(r"C:\ExampleProjects\llama.cpp\convert_hf_to_gguf.py")
PYTHON_BIN = Path(".venv/Scripts/python.exe").resolve()
DATA_PATH = Path("gemma_lora_data.json")
MAX_SEQ_LEN = 4096
NUM_EPOCHS = 3
BATCH_SIZE = 2
LR = 2e-4Делаем метод со всем остальным кодом:
def main() -> None:
...
if __name__ == "__main__":
import multiprocessing
multiprocessing.freeze_support()
main()Очистка папок после предыдущей работы скрипта:
print("[1/7] Cleaning previous artefacts…")
for _dir in (LORA_OUTPUT, MERGED_DIR, GGUF_DIR):
if _dir.exists():
shutil.rmtree(_dir)
print(f" ‑ removed «{_dir}»")
print("Finished cleaning")Чтение обучающих данных, здесь используется специфическое форматирование для моделей Gemma start_of_turn и end_of_turn, для других моделей оно может отличаться, примерный список:
Gemma -> <start_of_turn>message_text<end_of_turn> DeepSeek, Phi -> user: message_text Qwen 3, ChatML -> <|im_start|> message_text <|im_end|> LLaMA -> [INST] message_text [/INST] Command -> <|START_OF_TURN_TOKEN|> message_text <|END_OF_TURN_TOKEN|> OpenChat / Alpaca / Vicuna"-> ### User: message_text"
print("[2/7] Reading training examples…")
with DATA_PATH.open(encoding="utf‑8") as fp:
records = json.load(fp)
# Форматирует каждый пример как чередование "вопрос-ответ" в стиле чата
def build_chat(example: dict) -> dict:
prompt = example["prompt"].strip()
response = example["response"].strip()
return {
"text": (
f"<start_of_turn>user\n{prompt}<end_of_turn>\n"
f"<start_of_turn>model\n{response}<end_of_turn>\n"
)
}
# Создаёт HuggingFace Dataset из этих отформатированных строк
dataset = Dataset.from_list([build_chat(r) for r in records])
print(f"Loaded {len(dataset):,} samples")Загрузка и подготовка модели:
print(f"[3/7] Loading base model from «{MODEL_DIR}» …")
# Загружает модель (например, Gemma 3B) и токенизатор.
# Добавляет LoRA адаптацию (с автоопределением целевых слоёв).
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=str(MODEL_DIR),
)
model = FastLanguageModel.get_peft_model(model)
print("Model ready for fine‑tuning")Обучение:
print("[4/7] Starting supervised fine‑tuning …")
sft_cfg = SFTConfig(
per_device_train_batch_size=BATCH_SIZE,
num_train_epochs=NUM_EPOCHS,
learning_rate=LR,
logging_steps=10,
dataset_num_proc=1,
)
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
args=sft_cfg,
)
trainer.train()
print("Training complete")Сохранение LoRA весов в папку lora:
print(f"[5/7] Saving LoRA weights to «{LORA_OUTPUT}» …")
LORA_OUTPUT.mkdir(parents=True, exist_ok=True)
model.save_pretrained(
str(LORA_OUTPUT),
tokenizer,
save_method="lora"
)
print("Adapters saved")Объединение LoRA и базовой модели:
print("[6/7] Merging LoRA + base ⟶ fp16 …")
MERGED_DIR.mkdir(parents=True, exist_ok=True)
model.save_pretrained_merged(
str(MERGED_DIR),
tokenizer,
safe_serialization=True,
save_method="merged_16bit"
)
print(f"Merged model written to «{MERGED_DIR}»")Конвертация в GGUF, Запускает скрипт convert_hf_to_gguf.py из llama.cpp, чтобы конвертировать модель в формат GGUF с квантованием q8_0 для использования в LM Studio, llama.cpp и подобных фреймворках.
print("[7/7] Converting to GGUF (q8_0) …")
GGUF_DIR.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
str(PYTHON_BIN),
str(GGUF_CONVERTER),
os.path.abspath(MERGED_CLEAN_DIR),
"--outfile", str(GGUF_PATH),
"--outtype", "q8_0",
],
check=True,
)
print(f"GGUF ready at «{GGUF_PATH}»")
print("Pipeline finished successfully!")Полный код примера:
# ------------------------ Model in folder = google/gemma-3-4b-it ---------------------------
import torch
print(torch.__version__)
print(torch.version.cuda)
print(torch.cuda.is_available())
import os
os.environ["UNSLOTH_PATCH_RL_TRAINERS"] = "false"
os.environ["UNSLOTH_COMPILE_DISABLE"] = "1"
os.environ["TORCHINDUCTOR_DISABLED"] = "1"
import json
import shutil
import subprocess
from pathlib import Path
from datasets import Dataset
from unsloth import FastLanguageModel
from trl import SFTTrainer, SFTConfig
MODEL_DIR = Path("./model")
LORA_OUTPUT = Path("./lora")
MERGED_DIR = Path("./merged")
GGUF_DIR = Path("./gguf")
GGUF_PATH = GGUF_DIR / "model.gguf"
GGUF_CONVERTER = Path(r"C:\ExampleProjects\llama.cpp\convert_hf_to_gguf.py")
PYTHON_BIN = Path(".venv/Scripts/python.exe").resolve()
DATA_PATH = Path("gemma_lora_data.json")
MAX_SEQ_LEN = 4096
NUM_EPOCHS = 3
BATCH_SIZE = 2
LR = 2e-4
def main():
print("[1/7] Cleaning previous artefacts…")
for _dir in (LORA_OUTPUT, MERGED_DIR, GGUF_DIR):
if _dir.exists():
shutil.rmtree(_dir)
print(f" ‑ removed «{_dir}»")
print("Finished cleaning")
print("[2/7] Reading training examples…")
with DATA_PATH.open(encoding="utf-8") as fp:
records = json.load(fp)
def build_chat(example):
prompt = example["prompt"].strip()
response = example["response"].strip()
return {
"text": (
f"<start_of_turn>user\n{prompt}<end_of_turn>\n"
f"<start_of_turn>model\n{response}<end_of_turn>\n"
)
}
dataset = Dataset.from_list([build_chat(r) for r in records])
print(f"Loaded {len(dataset):,} samples")
print(f"[3/7] Loading base model from «{MODEL_DIR}» …")
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=str(MODEL_DIR),
)
model = FastLanguageModel.get_peft_model(model)
print("Model ready for fine‑tuning")
print("[4/7] Starting supervised fine‑tuning …")
sft_cfg = SFTConfig(
per_device_train_batch_size=BATCH_SIZE,
num_train_epochs=NUM_EPOCHS,
learning_rate=LR,
logging_steps=10,
dataset_num_proc=1,
)
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
args=sft_cfg,
)
trainer.train()
print("Training complete")
print(f"[5/7] Saving LoRA weights to «{LORA_OUTPUT}» …")
LORA_OUTPUT.mkdir(parents=True, exist_ok=True)
model.save_pretrained(
str(LORA_OUTPUT),
tokenizer,
save_method="lora"
)
print("Adapters saved")
print("[6/7] Merging LoRA + base ⟶ fp16 …")
MERGED_DIR.mkdir(parents=True, exist_ok=True)
model.save_pretrained_merged(
str(MERGED_DIR),
tokenizer,
safe_serialization=True,
save_method="merged_16bit"
)
print(f"Merged model written to «{MERGED_DIR}»")
print("[7/7] Converting to GGUF (q8_0) …")
GGUF_DIR.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
str(PYTHON_BIN),
str(GGUF_CONVERTER),
os.path.abspath(MERGED_DIR),
"--outfile", str(GGUF_PATH),
"--outtype", "q8_0",
],
check=True,
)
print(f"GGUF ready at «{GGUF_PATH}»")
print("Pipeline finished successfully!")
if __name__ == "__main__":
import multiprocessing
multiprocessing.freeze_support()
main()
Текстовый выводи примера, этап обучения:
[4/7] Starting supervised fine‑tuning …
Unsloth: Tokenizing ["text"]: 100%|██████████| 91/91 [00:00<00:00, 10075.01 examples/s]
==((====))== Unsloth - 2x faster free finetuning | Num GPUs used = 1
\\ /| Num examples = 91 | Num Epochs = 3 | Total steps = 69
O^O/ \_/ \ Batch size per device = 2 | Gradient accumulation steps = 2
\ / Data Parallel GPUs = 1 | Total batch size (2 x 2 x 1) = 4
"-____-" Trainable parameters = 32,788,480 of 4,332,867,952 (0.76% trained)
0%| | 0/69 [00:00<?, ?it/s]`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`.
14%|█▍ | 10/69 [00:12<01:11, 1.21s/it]{'loss': 17.6186, 'grad_norm': 16.65082550048828, 'learning_rate': 0.00019354838709677422, 'epoch': 0.43}
29%|██▉ | 20/69 [00:24<00:57, 1.18s/it]{'loss': 3.6136, 'grad_norm': 6.085631847381592, 'learning_rate': 0.00016129032258064516, 'epoch': 0.87}
43%|████▎ | 30/69 [00:36<00:47, 1.21s/it]{'loss': 1.4697, 'grad_norm': 3.163569211959839, 'learning_rate': 0.00012903225806451613, 'epoch': 1.3}
58%|█████▊ | 40/69 [00:48<00:34, 1.19s/it]{'loss': 1.2614, 'grad_norm': 4.380921840667725, 'learning_rate': 9.677419354838711e-05, 'epoch': 1.74}
72%|███████▏ | 50/69 [01:00<00:23, 1.24s/it]{'loss': 1.0062, 'grad_norm': 3.690117835998535, 'learning_rate': 6.451612903225807e-05, 'epoch': 2.17}
87%|████████▋ | 60/69 [01:13<00:11, 1.31s/it]{'loss': 0.7645, 'grad_norm': 4.235830783843994, 'learning_rate': 3.2258064516129034e-05, 'epoch': 2.61}
100%|██████████| 69/69 [01:24<00:00, 1.23s/it]
{'train_runtime': 84.8047, 'train_samples_per_second': 3.219, 'train_steps_per_second': 0.814, 'train_loss': 3.8283379941746807, 'epoch': 3.0}
Training completePeft:
импорты:
import os
import json
import torch
import shutil
import subprocess
import safetensors.torch
from pathlib import Path
from datasets import Dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling,
BitsAndBytesConfig,
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training
)Константы, путь к скрипту GGUF_CONVERTER у вас может отличаться:
MODEL_DIR = Path("./model")
LORA_OUTPUT = Path("./lora")
MERGED_DIR = Path("./merged")
MERGED_CLEAN_DIR = Path("./merged_clean")
GGUF_DIR = Path("./gguf")
GGUF_PATH = GGUF_DIR / "model.gguf"
GGUF_CONVERTER = Path(r"C:\GitHub\llama.cpp\convert_hf_to_gguf.py")
PYTHON_BIN = Path(".venv/Scripts/python.exe").resolve()
DATA_PATH = Path("gemma_lora_data.json")
MAX_SEQ_LEN = 4096
NUM_EPOCHS = 3
BATCH_SIZE = 2
LR = 2e-4
LORA_R = 8
LORA_ALPHA = 16
LORA_DROPOUT = 0.05Делаем метод со всем остальным кодом:
def main() -> None:
...
if __name__ == "__main__":
import multiprocessing
multiprocessing.freeze_support()
main()Очистка папок после предыдущей работы скрипта:
print("[1/7] Cleaning previous artefacts…")
for _dir in (LORA_OUTPUT, MERGED_DIR, GGUF_DIR):
if _dir.exists():
shutil.rmtree(_dir)
print(f" ‑ removed «{_dir}»")
print("Finished cleaning")Чтение обучающих данных, здесь используется специфическое форматирование для моделей Gemma start_of_turn и end_of_turn, для других моделей оно может отличаться, примерный список:
Gemma -> <start_of_turn>message_text<end_of_turn> DeepSeek, Phi -> user: message_text Qwen 3, ChatML -> <|im_start|> message_text <|im_end|> LLaMA -> [INST] message_text [/INST] Command -> <|START_OF_TURN_TOKEN|> message_text <|END_OF_TURN_TOKEN|> OpenChat / Alpaca / Vicuna"-> ### User: message_text"
print("[2/7] Reading training examples…")
with DATA_PATH.open(encoding="utf‑8") as fp:
records = json.load(fp)
# Форматирует каждый пример как чередование "вопрос-ответ" в стиле чата
def build_chat(example: dict) -> dict:
prompt = example["prompt"].strip()
response = example["response"].strip()
return {
"text": (
f"<start_of_turn>user\n{prompt}<end_of_turn>\n"
f"<start_of_turn>model\n{response}<end_of_turn>\n"
)
}
# Создаёт HuggingFace Dataset из этих отформатированных строк
dataset = Dataset.from_list([build_chat(r) for r in records])
print(f"Loaded {len(dataset):,} samples")Загрузка и подготовка модели:
print(f"[3/7] Loading base model from «{MODEL_DIR}» …")
# Настраивает загрузку модели с 4-битной квантизацией (nf4)
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
load_in_8bit=False,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
llm_int8_threshold=6.0,
llm_int8_skip_modules=None,
llm_int8_enable_fp32_cpu_offload=False,
llm_int8_has_fp16_weight=False,
bnb_4bit_quant_storage=torch.uint8
)
# Загружает токенизатор и квантизированную модель
tokenizer = AutoTokenizer.from_pretrained(
MODEL_DIR,
trust_remote_code=True
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
model = AutoModelForCausalLM.from_pretrained(
MODEL_DIR,
device_map="auto",
quantization_config=bnb_config,
torch_dtype=torch.bfloat16,
)
# Подготавливает модель к обучению с LoRA, размораживает нужные слои
model = prepare_model_for_kbit_training(model)
# Настраивает LoRA-адаптацию для заданных слоёв (q_proj, k_proj и др.)
lora_config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
lora_dropout=LORA_DROPOUT,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
],
)
model = get_peft_model(model, lora_config)
print("Model ready for fine‑tuning")Обучение:
print("[4/7] Starting supervised fine‑tuning …")
# Токенизирует текстовые примеры
def tokenize(example):
tokens = tokenizer(
example["text"],
truncation=True,
max_length=MAX_SEQ_LEN,
)
return tokens
ds_tok = dataset.map(tokenize, batched=True, remove_columns=["text"])
# Создаёт дата-коллатор, задаёт параметры обучения и запускает fine-tuning
collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False,
)
args = TrainingArguments(
per_device_train_batch_size=BATCH_SIZE,
gradient_accumulation_steps=4,
num_train_epochs=NUM_EPOCHS,
learning_rate=LR,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=10,
save_strategy="epoch",
bf16=True,
optim="paged_adamw_8bit",
report_to="none",
)
trainer = Trainer(
model=model,
args=args,
train_dataset=ds_tok,
data_collator=collator,
)
trainer.train()
print("Training complete")Сохранение LoRA весов в папку lora:
print(f"[5/7] Saving LoRA weights to «{LORA_OUTPUT}» …")
LORA_OUTPUT.mkdir(parents=True, exist_ok=True)
model.save_pretrained(
str(LORA_OUTPUT),
save_method="lora"
)
print("Adapters saved")Объединение LoRA и базовой модели:
print("[6/7] Merging LoRA + base ⟶ fp16 …")
model = model.merge_and_unload()
MERGED_DIR.mkdir(parents=True, exist_ok=True)
model.save_pretrained(
MERGED_DIR,
safe_serialization=True,
save_method="merged_16bit"
)
tokenizer.save_pretrained(MERGED_DIR)
print(f"Merged model written to «{MERGED_DIR}»")Очистка объединённой модели от служебных данных, полученных в ходе дообучения:
print("[6.5/7] Creating cleaned model in «merged_clean» …")
# Копирует все файлы, кроме весов, в новую директорию merged_clean.
MERGED_CLEAN_DIR.mkdir(parents=True, exist_ok=True)
for file in MERGED_DIR.iterdir():
if file.name != "model.safetensors":
shutil.copy(file, MERGED_CLEAN_DIR / file.name)
model_path = MERGED_DIR / "model.safetensors"
clean_path = MERGED_CLEAN_DIR / "model.safetensors"
# Сохраняет очищенную версию весов без технических артефактов, только веса модели.
state_dict = safetensors.torch.load_file(str(model_path))
import re
pattern = re.compile(
r".*\.(absmax|zeros|scales|quant_map|quant_state(\..+)?|nested_absmax|nested_zeros|nested_scales|nested_quant_map)$"
)
keys_to_remove = [k for k in state_dict if pattern.match(k)]
for key in keys_to_remove:
del state_dict[key]
safetensors.torch.save_file(state_dict, str(clean_path), metadata={"format": "pt"})
print(f"Saved cleaned model to «{MERGED_CLEAN_DIR}», removed {len(keys_to_remove)} keys")Конвертация в GGUF, Запускает скрипт convert_hf_to_gguf.py из llama.cpp, чтобы конвертировать модель в формат GGUF с квантованием q8_0 для использования в LM Studio, llama.cpp и подобных фреймворках.
print("[7/7] Converting to GGUF (q8_0) …")
GGUF_DIR.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
str(PYTHON_BIN),
str(GGUF_CONVERTER),
os.path.abspath(MERGED_CLEAN_DIR),
"--outfile", str(GGUF_PATH),
"--outtype", "q8_0",
],
check=True,
)
print(f"GGUF ready at «{GGUF_PATH}»")
print("Pipeline finished successfully!")Полный код примера:
import os
import json
import torch
import shutil
import subprocess
import safetensors.torch
from pathlib import Path
from datasets import Dataset
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling,
BitsAndBytesConfig,
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training
)
MODEL_DIR = Path("./model")
LORA_OUTPUT = Path("./lora")
MERGED_DIR = Path("./merged")
MERGED_CLEAN_DIR = Path("./merged_clean")
GGUF_DIR = Path("./gguf")
GGUF_PATH = GGUF_DIR / "model.gguf"
GGUF_CONVERTER = Path(r"C:\ExampleProjects\llama.cpp\convert_hf_to_gguf.py")
PYTHON_BIN = Path(".venv/Scripts/python.exe").resolve()
DATA_PATH = Path("gemma_lora_data.json")
MAX_SEQ_LEN = 4096
NUM_EPOCHS = 3
BATCH_SIZE = 2
LR = 2e-4
LORA_R = 8
LORA_ALPHA = 16
LORA_DROPOUT = 0.05
def main() -> None:
print("[1/7] Cleaning previous artefacts…")
for _dir in (LORA_OUTPUT, MERGED_DIR, GGUF_DIR):
if _dir.exists():
shutil.rmtree(_dir)
print(f" ‑ removed «{_dir}»")
print("Finished cleaning")
print("[2/7] Reading training examples…")
with DATA_PATH.open(encoding="utf‑8") as fp:
records = json.load(fp)
def build_chat(example: dict) -> dict:
prompt = example["prompt"].strip()
response = example["response"].strip()
return {
"text": (
f"<start_of_turn>user\n{prompt}<end_of_turn>\n"
f"<start_of_turn>model\n{response}<end_of_turn>\n"
)
}
dataset = Dataset.from_list([build_chat(r) for r in records])
print(f"Loaded {len(dataset):,} samples")
#
print(f"[3/7] Loading base model from «{MODEL_DIR}» …")
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
load_in_8bit=False,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
llm_int8_threshold=6.0,
llm_int8_skip_modules=None,
llm_int8_enable_fp32_cpu_offload=False,
llm_int8_has_fp16_weight=False,
bnb_4bit_quant_storage=torch.uint8
)
tokenizer = AutoTokenizer.from_pretrained(
MODEL_DIR,
trust_remote_code=True
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
model = AutoModelForCausalLM.from_pretrained(
MODEL_DIR,
device_map="auto",
quantization_config=bnb_config,
torch_dtype=torch.bfloat16,
)
model = prepare_model_for_kbit_training(model)
lora_config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
lora_dropout=LORA_DROPOUT,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
],
)
model = get_peft_model(model, lora_config)
print("Model ready for fine‑tuning")
print("[4/7] Starting supervised fine‑tuning …")
def tokenize(example):
tokens = tokenizer(
example["text"],
truncation=True,
max_length=MAX_SEQ_LEN,
)
return tokens
ds_tok = dataset.map(tokenize, batched=True, remove_columns=["text"])
collator = DataCollatorForLanguageModeling(
tokenizer=tokenizer,
mlm=False,
)
args = TrainingArguments(
per_device_train_batch_size=BATCH_SIZE,
gradient_accumulation_steps=4,
num_train_epochs=NUM_EPOCHS,
learning_rate=LR,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
logging_steps=10,
save_strategy="epoch",
bf16=True,
optim="paged_adamw_8bit",
report_to="none",
)
trainer = Trainer(
model=model,
args=args,
train_dataset=ds_tok,
data_collator=collator,
)
trainer.train()
print("Training complete")
print(f"[5/7] Saving LoRA weights to «{LORA_OUTPUT}» …")
LORA_OUTPUT.mkdir(parents=True, exist_ok=True)
model.save_pretrained(
str(LORA_OUTPUT),
save_method="lora"
)
print("Adapters saved")
print("[6/7] Merging LoRA + base ⟶ fp16 …")
model = model.merge_and_unload()
MERGED_DIR.mkdir(parents=True, exist_ok=True)
model.save_pretrained(
MERGED_DIR,
safe_serialization=True,
save_method="merged_16bit"
)
tokenizer.save_pretrained(MERGED_DIR)
print(f"Merged model written to «{MERGED_DIR}»")
print("[6.5/7] Creating cleaned model in «merged_clean» …")
MERGED_CLEAN_DIR.mkdir(parents=True, exist_ok=True)
for file in MERGED_DIR.iterdir():
if file.name != "model.safetensors":
shutil.copy(file, MERGED_CLEAN_DIR / file.name)
model_path = MERGED_DIR / "model.safetensors"
clean_path = MERGED_CLEAN_DIR / "model.safetensors"
state_dict = safetensors.torch.load_file(str(model_path))
import re
pattern = re.compile(
r".*\.(absmax|zeros|scales|quant_map|quant_state(\..+)?|nested_absmax|nested_zeros|nested_scales|nested_quant_map)$"
)
keys_to_remove = [k for k in state_dict if pattern.match(k)]
for key in keys_to_remove:
del state_dict[key]
safetensors.torch.save_file(state_dict, str(clean_path), metadata={"format": "pt"})
print(f"Saved cleaned model to «{MERGED_CLEAN_DIR}», removed {len(keys_to_remove)} keys")
print("[7/7] Converting to GGUF (q8_0) …")
GGUF_DIR.mkdir(parents=True, exist_ok=True)
subprocess.run(
[
str(PYTHON_BIN),
str(GGUF_CONVERTER),
os.path.abspath(MERGED_CLEAN_DIR),
"--outfile", str(GGUF_PATH),
"--outtype", "q8_0",
],
check=True,
)
print(f"GGUF ready at «{GGUF_PATH}»")
print("Pipeline finished successfully!")
if __name__ == "__main__":
import multiprocessing
multiprocessing.freeze_support()
main()
Текстовый выводи примера, этап обучения:
[4/7] Starting supervised fine‑tuning …
Map: 100%|██████████| 91/91 [00:00<00:00, 11339.66 examples/s]
No label_names provided for model class `PeftModelForCausalLM`. Since `PeftModel` hides base models input arguments, if label_names is not given, label_names can't be set automatically within `Trainer`. Note that empty label_names list will be used instead.
0%| | 0/36 [00:00<?, ?it/s]`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`.
C:\Users\Roman\PycharmProjects\LoRA_Example\.venv\Lib\site-packages\torch\_dynamo\eval_frame.py:632: UserWarning: torch.utils.checkpoint: the use_reentrant parameter should be passed explicitly. In version 2.5 we will raise an exception if use_reentrant is not passed. use_reentrant=False is recommended, but if you need to preserve the current default behavior, you can pass use_reentrant=True. Refer to docs for more details on the differences between the two variants.
return fn(*args, **kwargs)
28%|██▊ | 10/36 [00:27<01:07, 2.61s/it]{'loss': 26.8174, 'grad_norm': 22.677703857421875, 'learning_rate': 0.00017980172272802396, 'epoch': 0.87}
33%|███▎ | 12/36 [00:31<00:53, 2.24s/it]C:\Users\Roman\PycharmProjects\LoRA_Example\.venv\Lib\site-packages\torch\_dynamo\eval_frame.py:632: UserWarning: torch.utils.checkpoint: the use_reentrant parameter should be passed explicitly. In version 2.5 we will raise an exception if use_reentrant is not passed. use_reentrant=False is recommended, but if you need to preserve the current default behavior, you can pass use_reentrant=True. Refer to docs for more details on the differences between the two variants.
return fn(*args, **kwargs)
56%|█████▌ | 20/36 [00:53<00:43, 2.72s/it]{'loss': 4.588, 'grad_norm': 10.421425819396973, 'learning_rate': 0.0001, 'epoch': 1.7}
67%|██████▋ | 24/36 [01:02<00:27, 2.26s/it]C:\Users\Roman\PycharmProjects\LoRA_Example\.venv\Lib\site-packages\torch\_dynamo\eval_frame.py:632: UserWarning: torch.utils.checkpoint: the use_reentrant parameter should be passed explicitly. In version 2.5 we will raise an exception if use_reentrant is not passed. use_reentrant=False is recommended, but if you need to preserve the current default behavior, you can pass use_reentrant=True. Refer to docs for more details on the differences between the two variants.
return fn(*args, **kwargs)
83%|████████▎ | 30/36 [01:18<00:15, 2.58s/it]{'loss': 2.4059, 'grad_norm': 9.479026794433594, 'learning_rate': 2.0198277271976052e-05, 'epoch': 2.52}
100%|██████████| 36/36 [01:33<00:00, 2.60s/it]
{'train_runtime': 93.46, 'train_samples_per_second': 2.921, 'train_steps_per_second': 0.385, 'train_loss': 9.714792675442165, 'epoch': 3.0}
Training completeПочитать еще:
https://huggingface.co/learn/llm-course/chapter11/4
https://habr.com/ru/articles/860892
https://toashishagarwal.medium.com/how-to-fine-tune-a-llm-using-lora-5fdb6dea11a6
https://medium.com/@rachittayal7/my-experiences-with-finetuning-llms-using-lora-b9c90f1839c6