Привет
Если нужно, чтобы 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 complete
Peft:
импорты:
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