apoapps commited on
Commit
aecafaa
·
verified ·
1 Parent(s): 45b6416

Upload finetune_apochat_peft.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. finetune_apochat_peft.py +177 -0
finetune_apochat_peft.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Fine-tune Gemma 4 E2B on the Apochat chat dataset using PEFT/LoRA.
3
+
4
+ Designed to run on a Hugging Face GPU Space or any CUDA machine with ≥16 GB VRAM.
5
+ QLoRA mode (default) uses 4-bit quantization and should fit on a T4/V100.
6
+
7
+ Usage:
8
+ python scripts/finetune_apochat_peft.py \
9
+ --dataset apoapps/apochat-gemma4-e2b-chat-v1 \
10
+ --output-dir ./apochat-gemma4-e2b-peft \
11
+ --push-to-hub apoapps/apochat-gemma4-e2b-apochat-tuned-v2 \
12
+ --use-qlora
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import os
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ import torch
23
+ from datasets import load_dataset
24
+ from peft import LoraConfig, TaskType, get_peft_model, prepare_model_for_kbit_training
25
+ from transformers import (
26
+ AutoModelForCausalLM,
27
+ AutoTokenizer,
28
+ BitsAndBytesConfig,
29
+ DataCollatorForLanguageModeling,
30
+ TrainingArguments,
31
+ )
32
+ from trl import SFTTrainer
33
+
34
+
35
+ def parse_args() -> argparse.Namespace:
36
+ parser = argparse.ArgumentParser()
37
+ parser.add_argument("--base-model", default="google/gemma-4-E2B-it")
38
+ parser.add_argument("--dataset", default="apoapps/apochat-gemma4-e2b-chat-v1")
39
+ parser.add_argument("--output-dir", default="./apochat-gemma4-e2b-peft")
40
+ parser.add_argument("--push-to-hub", default=None, help="HF repo to push the adapter")
41
+ parser.add_argument("--use-qlora", action="store_true", help="Use 4-bit QLoRA (saves VRAM)")
42
+ parser.add_argument("--epochs", type=float, default=1.0)
43
+ parser.add_argument("--batch-size", type=int, default=1)
44
+ parser.add_argument("--gradient-accumulation-steps", type=int, default=4)
45
+ parser.add_argument("--learning-rate", type=float, default=2e-4)
46
+ parser.add_argument("--lora-r", type=int, default=16)
47
+ parser.add_argument("--lora-alpha", type=int, default=32)
48
+ parser.add_argument("--max-seq-length", type=int, default=1024)
49
+ return parser.parse_args()
50
+
51
+
52
+ def formatting_prompts_func(examples: dict, tokenizer) -> dict:
53
+ texts = []
54
+ for messages in examples["messages"]:
55
+ # messages is a list of {"role": ..., "content": ...}
56
+ text = tokenizer.apply_chat_template(
57
+ messages,
58
+ tokenize=False,
59
+ add_generation_prompt=False,
60
+ )
61
+ texts.append(text)
62
+ return {"text": texts}
63
+
64
+
65
+ def main() -> int:
66
+ args = parse_args()
67
+
68
+ tokenizer = AutoTokenizer.from_pretrained(args.base_model, trust_remote_code=True)
69
+ if tokenizer.pad_token is None:
70
+ tokenizer.pad_token = tokenizer.eos_token
71
+
72
+ bnb_config = None
73
+ torch_dtype = torch.bfloat16
74
+ if args.use_qlora:
75
+ bnb_config = BitsAndBytesConfig(
76
+ load_in_4bit=True,
77
+ bnb_4bit_quant_type="nf4",
78
+ bnb_4bit_compute_dtype=torch.bfloat16,
79
+ bnb_4bit_use_double_quant=True,
80
+ )
81
+ torch_dtype = torch.bfloat16
82
+
83
+ model = AutoModelForCausalLM.from_pretrained(
84
+ args.base_model,
85
+ quantization_config=bnb_config,
86
+ torch_dtype=torch_dtype if bnb_config is None else None,
87
+ device_map="auto",
88
+ trust_remote_code=True,
89
+ attn_implementation="eager", # safer for Gemma 4 + gradient checkpointing
90
+ )
91
+
92
+ lora_config = LoraConfig(
93
+ r=args.lora_r,
94
+ lora_alpha=args.lora_alpha,
95
+ target_modules=[
96
+ "q_proj",
97
+ "k_proj",
98
+ "v_proj",
99
+ "o_proj",
100
+ "gate_proj",
101
+ "up_proj",
102
+ "down_proj",
103
+ "per_layer_input_gate",
104
+ "per_layer_projection",
105
+ ],
106
+ lora_dropout=0.05,
107
+ bias="none",
108
+ task_type=TaskType.CAUSAL_LM,
109
+ )
110
+
111
+ if args.use_qlora:
112
+ model = prepare_model_for_kbit_training(model)
113
+ model = get_peft_model(model, lora_config)
114
+ model.print_trainable_parameters()
115
+
116
+ ds = load_dataset(args.dataset, data_files={"train": "train.jsonl", "valid": "valid.jsonl"})
117
+ train_ds = ds["train"]
118
+ valid_ds = ds["valid"] if "valid" in ds else None
119
+
120
+ train_ds = train_ds.map(
121
+ lambda x: formatting_prompts_func(x, tokenizer),
122
+ batched=True,
123
+ remove_columns=train_ds.column_names,
124
+ )
125
+ if valid_ds is not None:
126
+ valid_ds = valid_ds.map(
127
+ lambda x: formatting_prompts_func(x, tokenizer),
128
+ batched=True,
129
+ remove_columns=valid_ds.column_names,
130
+ )
131
+
132
+ output_dir = Path(args.output_dir)
133
+ output_dir.mkdir(parents=True, exist_ok=True)
134
+
135
+ training_args = TrainingArguments(
136
+ output_dir=str(output_dir),
137
+ num_train_epochs=args.epochs,
138
+ per_device_train_batch_size=args.batch_size,
139
+ gradient_accumulation_steps=args.gradient_accumulation_steps,
140
+ learning_rate=args.learning_rate,
141
+ bf16=True,
142
+ logging_steps=10,
143
+ evaluation_strategy="steps" if valid_ds is not None else "no",
144
+ eval_steps=200,
145
+ save_strategy="epoch",
146
+ save_total_limit=2,
147
+ push_to_hub=bool(args.push_to_hub),
148
+ hub_model_id=args.push_to_hub,
149
+ hub_private=True,
150
+ gradient_checkpointing=True,
151
+ optim="paged_adamw_8bit" if args.use_qlora else "adamw_torch",
152
+ report_to="none",
153
+ )
154
+
155
+ trainer = SFTTrainer(
156
+ model=model,
157
+ tokenizer=tokenizer,
158
+ train_dataset=train_ds,
159
+ eval_dataset=valid_ds,
160
+ max_seq_length=args.max_seq_length,
161
+ args=training_args,
162
+ dataset_text_field="text",
163
+ )
164
+
165
+ trainer.train()
166
+ trainer.save_model(str(output_dir / "final_adapter"))
167
+
168
+ if args.push_to_hub:
169
+ model.push_to_hub(args.push_to_hub, private=True)
170
+ tokenizer.push_to_hub(args.push_to_hub, private=True)
171
+
172
+ print(f"Training complete. Adapter saved to {output_dir / 'final_adapter'}")
173
+ return 0
174
+
175
+
176
+ if __name__ == "__main__":
177
+ sys.exit(main())