jacobeen06 commited on
Commit
3265eba
·
verified ·
1 Parent(s): cb1a436

DPO training script

Browse files
Files changed (1) hide show
  1. train_dpo.py +264 -0
train_dpo.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Genesis-2.0 — Phase 1: DPO Preference Alignment
4
+
5
+ Run on RunPod RTX PRO 6000 Blackwell (96 GB).
6
+
7
+ Steps:
8
+ 1. Load Qwen3.6-35B-A3B with QLoRA (NF4)
9
+ 2. Merge existing Genesis-1.0 SFT adapter
10
+ 3. Attach new all-linear LoRA (Config C: attn + shared + gate, r=32)
11
+ 4. Load DPO preference pairs
12
+ 5. Run DPO training
13
+ 6. Save + upload adapter to HuggingFace Hub
14
+
15
+ Usage:
16
+ python3 train_dpo.py
17
+ """
18
+
19
+ import os
20
+ import json
21
+ import sys
22
+ import torch
23
+ from typing import Optional
24
+
25
+ # ============================================================
26
+ # CONFIG
27
+ # ============================================================
28
+
29
+ class Config:
30
+ # Paths
31
+ base_model = "Qwen/Qwen3.6-35B-A3B"
32
+ sft_adapter = "jacobeen06/Genesis-1.0-SFT-adapter" # Genesis-1.0 SFT adapter
33
+ dpo_data = "/workspace/dpo_pairs_all.jsonl" # Will be uploaded
34
+ output_dir = "/workspace/genesis2-dpo"
35
+ hf_repo = "jacobeen06/Genesis-2.0-DPO-adapter"
36
+
37
+ # QLoRA
38
+ load_in_4bit = True
39
+ bnb_4bit_quant_type = "nf4"
40
+ bnb_4bit_compute_dtype = torch.bfloat16
41
+
42
+ # New all-linear LoRA (Config C)
43
+ lora_r = 32
44
+ lora_alpha = 64
45
+ lora_dropout = 0.0
46
+ # Attention modules
47
+ lora_target_modules = [
48
+ "q_proj", "k_proj", "v_proj", "o_proj",
49
+ # Shared expert FFN
50
+ "gate_proj", "up_proj", "down_proj",
51
+ # Gate/router
52
+ "gate",
53
+ ]
54
+ use_rslora = True
55
+
56
+ # DPO hyperparameters
57
+ beta = 0.1 # DPO temperature
58
+ learning_rate = 5e-6
59
+ lr_scheduler_type = "cosine"
60
+ warmup_ratio = 0.05
61
+ per_device_train_batch_size = 1
62
+ gradient_accumulation_steps = 4
63
+ num_train_epochs = 1
64
+ max_length = 4096
65
+ max_prompt_length = 3072
66
+ logging_steps = 10
67
+ save_steps = 200
68
+ eval_steps = 200
69
+ save_total_limit = 2
70
+ remove_unused_columns = False
71
+
72
+ # DeepSpeed / distributed
73
+ local_rank = int(os.environ.get("LOCAL_RANK", 0))
74
+ world_size = int(os.environ.get("WORLD_SIZE", 1))
75
+ deepspeed_config = None # Use if multi-GPU: "ds_config.json"
76
+
77
+
78
+ # ============================================================
79
+ # MODEL LOADING
80
+ # ============================================================
81
+
82
+ def load_model_and_tokenizer(config: Config):
83
+ """Load base model with QLoRA and merge the existing SFT adapter."""
84
+ import torch
85
+ from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
86
+ from peft import PeftModel, LoraConfig, get_peft_model
87
+
88
+ print(f"[{config.local_rank}] Loading base model: {config.base_model}")
89
+
90
+ # Quantization config
91
+ bnb_config = BitsAndBytesConfig(
92
+ load_in_4bit=config.load_in_4bit,
93
+ bnb_4bit_quant_type=config.bnb_4bit_quant_type,
94
+ bnb_4bit_compute_dtype=config.bnb_4bit_compute_dtype,
95
+ )
96
+
97
+ model = AutoModelForCausalLM.from_pretrained(
98
+ config.base_model,
99
+ quantization_config=bnb_config,
100
+ device_map="auto" if config.world_size == 1 else {"": config.local_rank},
101
+ trust_remote_code=True,
102
+ torch_dtype=torch.bfloat16,
103
+ )
104
+
105
+ tokenizer = AutoTokenizer.from_pretrained(
106
+ config.base_model,
107
+ trust_remote_code=True,
108
+ )
109
+ if tokenizer.pad_token is None:
110
+ tokenizer.pad_token = tokenizer.eos_token
111
+ tokenizer.pad_token_id = tokenizer.eos_token_id
112
+
113
+ # ---- Merge Genesis-1.0 SFT adapter ----
114
+ print(f"[{config.local_rank}] Loading Genesis-1.0 SFT adapter...")
115
+ model = PeftModel.from_pretrained(model, config.sft_adapter)
116
+ print(f"[{config.local_rank}] Merging SFT adapter into base...")
117
+ model = model.merge_and_unload()
118
+ print(f"[{config.local_rank}] SFT adapter merged. Model type: {type(model).__name__}")
119
+
120
+ # ---- Attach new all-linear LoRA ----
121
+ print(f"[{config.local_rank}] Attaching new all-linear LoRA (r={config.lora_r})...")
122
+ lora_config = LoraConfig(
123
+ r=config.lora_r,
124
+ lora_alpha=config.lora_alpha,
125
+ target_modules=config.lora_target_modules,
126
+ lora_dropout=config.lora_dropout,
127
+ bias="none",
128
+ task_type="CAUSAL_LM",
129
+ use_rslora=config.use_rslora,
130
+ )
131
+ model = get_peft_model(model, lora_config)
132
+ model.print_trainable_parameters()
133
+
134
+ return model, tokenizer
135
+
136
+
137
+ # ============================================================
138
+ # DATA LOADING
139
+ # ============================================================
140
+
141
+ def load_dpo_data(config: Config):
142
+ """Load DPO pairs from JSONL."""
143
+ from datasets import Dataset
144
+
145
+ if not os.path.exists(config.dpo_data):
146
+ print(f"[{config.local_rank}] ERROR: {config.dpo_data} not found!")
147
+ print("Upload dpo_pairs_all.jsonl to /workspace/ on the pod first.")
148
+ sys.exit(1)
149
+
150
+ pairs = []
151
+ with open(config.dpo_data) as f:
152
+ for line in f:
153
+ line = line.strip()
154
+ if line:
155
+ pairs.append(json.loads(line))
156
+
157
+ print(f"[{config.local_rank}] Loaded {len(pairs)} DPO pairs from {config.dpo_data}")
158
+
159
+ # Convert to dataset
160
+ dataset = Dataset.from_list(pairs)
161
+ return dataset
162
+
163
+
164
+ # ============================================================
165
+ # TRAINING
166
+ # ============================================================
167
+
168
+ def train_dpo(config: Config):
169
+ from transformers import TrainingArguments
170
+ from trl import DPOTrainer
171
+
172
+ model, tokenizer = load_model_and_tokenizer(config)
173
+ dataset = load_dpo_data(config)
174
+
175
+ # Split train/eval
176
+ split_dataset = dataset.train_test_split(test_size=0.05, seed=42)
177
+ train_dataset = split_dataset["train"]
178
+ eval_dataset = split_dataset["test"]
179
+
180
+ print(f"[{config.local_rank}] Train: {len(train_dataset)}, Eval: {len(eval_dataset)}")
181
+
182
+ # Training args
183
+ training_args = TrainingArguments(
184
+ output_dir=config.output_dir,
185
+ per_device_train_batch_size=config.per_device_train_batch_size,
186
+ gradient_accumulation_steps=config.gradient_accumulation_steps,
187
+ learning_rate=config.learning_rate,
188
+ lr_scheduler_type=config.lr_scheduler_type,
189
+ warmup_ratio=config.warmup_ratio,
190
+ num_train_epochs=config.num_train_epochs,
191
+ logging_steps=config.logging_steps,
192
+ save_steps=config.save_steps,
193
+ eval_steps=config.eval_steps,
194
+ eval_strategy="steps" if len(eval_dataset) > 0 else "no",
195
+ save_total_limit=config.save_total_limit,
196
+ remove_unused_columns=config.remove_unused_columns,
197
+ bf16=True,
198
+ tf32=True,
199
+ gradient_checkpointing=True,
200
+ gradient_checkpointing_kwargs={"use_reentrant": False},
201
+ logging_dir=os.path.join(config.output_dir, "logs"),
202
+ report_to="wandb" if os.environ.get("WANDB_API_KEY") else "none",
203
+ run_name="genesis2-dpo",
204
+ ddp_find_unused_parameters=False if config.world_size > 1 else None,
205
+ dataloader_num_workers=2,
206
+ )
207
+
208
+ # DPO Trainer
209
+ trainer = DPOTrainer(
210
+ model=model,
211
+ ref_model=None, # Will use a frozen copy internally
212
+ args=training_args,
213
+ train_dataset=train_dataset,
214
+ eval_dataset=eval_dataset,
215
+ tokenizer=tokenizer,
216
+ beta=config.beta,
217
+ max_length=config.max_length,
218
+ max_prompt_length=config.max_prompt_length,
219
+ )
220
+
221
+ # Train
222
+ print(f"[{config.local_rank}] Starting DPO training...")
223
+ trainer.train()
224
+
225
+ # Save
226
+ print(f"[{config.local_rank}] Saving model to {config.output_dir}")
227
+ trainer.save_model(config.output_dir)
228
+ tokenizer.save_pretrained(config.output_dir)
229
+
230
+ # Upload to HF Hub
231
+ try:
232
+ from huggingface_hub import HfApi
233
+ api = HfApi()
234
+ api.create_repo(config.hf_repo, exist_ok=True)
235
+ api.upload_folder(
236
+ folder_path=config.output_dir,
237
+ repo_id=config.hf_repo,
238
+ commit_message="Genesis-2.0 DPO adapter",
239
+ )
240
+ print(f"[{config.local_rank}] Uploaded to {config.hf_repo}")
241
+ except Exception as e:
242
+ print(f"[{config.local_rank}] Upload failed (non-fatal): {e}")
243
+
244
+ print(f"[{config.local_rank}] DONE! Adapter saved to {config.output_dir}")
245
+ return trainer
246
+
247
+
248
+ # ============================================================
249
+ # MAIN
250
+ # ============================================================
251
+
252
+ if __name__ == "__main__":
253
+ config = Config()
254
+
255
+ print("=" * 60)
256
+ print("Genesis-2.0 — Phase 1: DPO Preference Alignment")
257
+ print("=" * 60)
258
+ print(f"Base model: {config.base_model}")
259
+ print(f"LoRA: r={config.lora_r}, targets={config.lora_target_modules}")
260
+ print(f"DPO data: {config.dpo_data}")
261
+ print(f"Output: {config.output_dir}")
262
+ print()
263
+
264
+ train_dpo(config)