ogx786 commited on
Commit
ea0ffd3
·
verified ·
1 Parent(s): 9abadde

Create finetune.py

Browse files
Files changed (1) hide show
  1. finetune.py +206 -212
finetune.py CHANGED
@@ -1,38 +1,32 @@
 
1
  """
2
- ============================================================
3
- Urdu -> Roman Urdu Fine-Tuning Script (CPU / Offline / HBL)
4
- ============================================================
5
- Converted from tiny-aya-ft-v2.ipynb for offline, CPU-only,
6
- air-gapped execution on the HBL server.
7
-
8
- Key differences from the original notebook:
9
- - No internet access anywhere (HF_HUB_OFFLINE=1)
10
- - No bitsandbytes / 4-bit quantization (CPU doesn't support it)
11
- - No push_to_hub / HF login (nothing leaves this machine)
12
- - Loads YOUR local data.csv (columns: urdu,roman) instead of
13
- the HF Roman-Urdu-Parl-split dataset + synthetic generator
14
- - Loads your already-finetuned model from a local folder
15
- instead of downloading CohereLabs/tiny-aya-global
16
-
17
- Run with:
18
- C:\\Python311\\python.exe finetune_hbl.py
19
- ============================================================
20
  """
21
 
22
  import os
23
 
24
- # ------------------------------------------------------------
25
- # MUST be set before importing transformers/datasets/huggingface_hub
26
- # ------------------------------------------------------------
27
  os.environ["HF_HUB_OFFLINE"] = "1"
28
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
 
 
29
 
30
  import gc
31
  import re
32
- import random
33
  import unicodedata
34
  import pandas as pd
35
  import torch
 
36
  from datasets import Dataset
37
  from transformers import (
38
  AutoTokenizer,
@@ -40,72 +34,81 @@ from transformers import (
40
  TrainingArguments,
41
  Trainer,
42
  )
43
- from peft import LoraConfig, get_peft_model, PeftModel
44
-
45
- random.seed(42)
 
 
46
 
47
  # ============================================================
48
- # CONFIG - edit these paths/values for your setup
49
  # ============================================================
50
 
51
- # Path to your ALREADY fine-tuned tiny-aya-global model folder
52
- # (the one you copied over earlier, confirmed working)
53
- MODEL_PATH = r"D:\urdu_model\your_finetuned_tiny_aya_model"
54
-
55
- # Path to your HBL dataset: CSV with header "urdu,roman"
56
- DATA_PATH = r"D:\urdu_model\data.csv"
57
-
58
- # Where to save the new LoRA adapter + merged model
59
- OUTPUT_DIR = r"D:\urdu_model\hbl_finetune_output"
60
-
61
- # --------------------------------------------------------------------
62
- # ROW LIMIT - IMPORTANT for CPU training
63
- # --------------------------------------------------------------------
64
- # CPU fine-tuning is roughly 20-50x slower than a T4 GPU. As a rough
65
- # guide on a modern multi-core CPU with this small model:
66
- # 500 rows -> a sanity-test run, a few minutes
67
- # 2,000 rows -> ~30-60 minutes, good for a first real pass
68
- # 5,000 rows -> a few hours
69
- # 10,000+ rows -> likely half a day or more; only do this once
70
- # you've confirmed the pipeline works end-to-end
71
- #
72
- # Start SMALL (MAX_ROWS = 500) to sanity check, then raise it.
73
- MAX_ROWS = 2000
74
-
75
- # Fraction of MAX_ROWS held out for validation
76
- VAL_FRACTION = 0.1
77
-
78
- # Training hyperparameters (scaled down for CPU)
 
 
 
 
 
 
79
  NUM_EPOCHS = 1
80
- PER_DEVICE_BATCH_SIZE = 1 # lowered from 2 -> less RAM per step
81
- GRAD_ACCUM_STEPS = 16 # effective batch size = 1 * 16 = 16 (unchanged)
82
  LEARNING_RATE = 2e-4
83
- MAX_SEQ_LENGTH = 224
 
 
 
 
 
84
 
85
  # ============================================================
86
- # 1. LOAD YOUR HBL DATASET
87
  # ============================================================
88
  print("=" * 60)
89
- print("Loading HBL dataset...")
90
  print("=" * 60)
91
 
92
- df = pd.read_csv(DATA_PATH)
93
- df = df.rename(columns={"urdu": "Urdu_Input", "roman": "Roman_Urdu_Target"})
94
- df = df.dropna(subset=["Urdu_Input", "Roman_Urdu_Target"])
 
 
 
95
 
96
- print(f"Total rows available in data.csv: {len(df):,}")
 
97
 
98
- if len(df) > MAX_ROWS:
99
- df = df.sample(n=MAX_ROWS, random_state=42).reset_index(drop=True)
100
- print(f"Sampled down to MAX_ROWS = {MAX_ROWS:,}")
101
  else:
102
- print(f"Using all {len(df):,} rows (fewer than MAX_ROWS limit)")
103
-
104
- # ============================================================
105
- # 2. CLEAN DATA
106
- # ============================================================
107
- URDU_COL = "Urdu_Input"
108
- ROMAN_COL = "Roman_Urdu_Target"
109
 
110
 
111
  def clean_urdu(s):
@@ -118,6 +121,7 @@ def clean_urdu(s):
118
 
119
 
120
  def clean_roman(s):
 
121
  if not isinstance(s, str) or len(s.strip()) == 0:
122
  return ""
123
  s = unicodedata.normalize("NFC", s)
@@ -128,63 +132,79 @@ def clean_roman(s):
128
  return s.upper()
129
 
130
 
131
- def preprocess_df(df, max_urdu_len=200, max_roman_len=250):
132
- df = df.copy()
133
- df[URDU_COL] = df[URDU_COL].apply(clean_urdu)
134
- df[ROMAN_COL] = df[ROMAN_COL].apply(clean_roman)
135
- df = df[(df[URDU_COL].str.len() > 0) & (df[ROMAN_COL].str.len() > 0)]
136
- df = df.drop_duplicates(subset=[URDU_COL, ROMAN_COL])
137
- df = df[df[URDU_COL].str.len() <= max_urdu_len]
138
- df = df[df[ROMAN_COL].str.len() <= max_roman_len]
139
- return df.reset_index(drop=True)
140
 
 
 
141
 
142
- df = preprocess_df(df)
143
- print(f"After cleaning: {len(df):,} usable rows")
144
 
145
- # Train/val split
146
- val_size = max(1, int(len(df) * VAL_FRACTION))
147
- val_df = df.iloc[:val_size].reset_index(drop=True)
148
- train_df = df.iloc[val_size:].reset_index(drop=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
 
150
- print(f"Train: {len(train_df):,} | Val: {len(val_df):,}")
151
- print("\nSample cleaned row:")
152
- print(f" Urdu : {train_df.iloc[0][URDU_COL]}")
153
- print(f" Roman: {train_df.iloc[0][ROMAN_COL]}")
154
 
155
  # ============================================================
156
- # 3. LOAD MODEL + TOKENIZER (LOCAL, OFFLINE)
157
  # ============================================================
158
  print("\n" + "=" * 60)
159
- print("Loading local model and tokenizer...")
160
  print("=" * 60)
161
 
162
- tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH)
163
- tokenizer.pad_token = tokenizer.eos_token
164
- tokenizer.padding_side = "right"
165
 
 
 
166
  model = AutoModelForCausalLM.from_pretrained(
167
- MODEL_PATH,
168
- torch_dtype=torch.bfloat16, # CPU-safe, ~half the RAM of float32
 
169
  trust_remote_code=True,
 
170
  )
171
  model.config.use_cache = False
172
- model.gradient_checkpointing_enable() # trades compute for much lower RAM use
 
 
173
 
174
- # ============================================================
175
- # 4. LoRA CONFIG (no quantization - CPU doesn't support bitsandbytes)
176
- # ============================================================
177
  lora_config = LoraConfig(
178
- r=16, # lowered from 32 -> lighter for CPU training
179
- lora_alpha=32,
180
  target_modules=[
181
- "q_proj",
182
- "k_proj",
183
- "v_proj",
184
- "o_proj",
185
- "gate_proj",
186
- "up_proj",
187
- "down_proj",
188
  ],
189
  lora_dropout=0.05,
190
  bias="none",
@@ -192,39 +212,18 @@ lora_config = LoraConfig(
192
  )
193
 
194
  model = get_peft_model(model, lora_config)
195
- print("LoRA adapter attached.")
196
  model.print_trainable_parameters()
197
 
 
198
  # ============================================================
199
- # 5. FORMAT DATA FOR INSTRUCTION TUNING
200
  # ============================================================
201
- def format_example(urdu, roman):
202
- return f"""### Instruction:
203
- Transliterate the following Urdu text into Roman Urdu.
204
- Output ONLY the Roman Urdu. No translation. No explanation.
205
-
206
- ### Input:
207
- {urdu}
208
-
209
- ### Response:
210
- {roman}{tokenizer.eos_token}"""
211
-
212
-
213
- train_texts = [format_example(u, r) for u, r in zip(train_df[URDU_COL], train_df[ROMAN_COL])]
214
- val_texts = [format_example(u, r) for u, r in zip(val_df[URDU_COL], val_df[ROMAN_COL])]
215
-
216
- train_dataset = Dataset.from_dict({"text": train_texts})
217
- val_dataset = Dataset.from_dict({"text": val_texts})
218
-
219
- print(f"\nTrain samples: {len(train_dataset):,}")
220
- print(f"Val samples: {len(val_dataset):,}")
221
-
222
-
223
  def tokenize_function(examples):
224
  return tokenizer(
225
  examples["text"],
226
  truncation=True,
227
- max_length=MAX_SEQ_LENGTH,
228
  padding=False,
229
  return_tensors=None,
230
  )
@@ -233,21 +232,15 @@ def tokenize_function(examples):
233
  tokenized_train = train_dataset.map(tokenize_function, batched=True, remove_columns=["text"])
234
  tokenized_val = val_dataset.map(tokenize_function, batched=True, remove_columns=["text"])
235
 
236
- # ============================================================
237
- # 6. CUSTOM COLLATOR - mask prompt, train only on Roman Urdu response
238
- # ============================================================
239
- response_template = "### Response:\n"
240
- response_ids = tokenizer.encode(response_template, add_special_tokens=False)
241
 
242
 
243
  def custom_completion_collator(features):
244
  batch = tokenizer.pad(features, return_tensors="pt")
245
  labels = batch["input_ids"].clone()
246
-
247
- # Mask padding tokens
248
  labels[batch["attention_mask"] == 0] = -100
249
 
250
- # Mask everything up to and including "### Response:\n"
251
  window_size = len(response_ids)
252
  for i in range(labels.shape[0]):
253
  label_seq = labels[i].tolist()
@@ -264,7 +257,7 @@ def custom_completion_collator(features):
264
 
265
 
266
  # ============================================================
267
- # 7. TRAINING ARGUMENTS (CPU-safe)
268
  # ============================================================
269
  training_args = TrainingArguments(
270
  output_dir=OUTPUT_DIR,
@@ -273,21 +266,22 @@ training_args = TrainingArguments(
273
  gradient_accumulation_steps=GRAD_ACCUM_STEPS,
274
  num_train_epochs=NUM_EPOCHS,
275
  learning_rate=LEARNING_RATE,
276
- warmup_steps=20,
277
- logging_steps=5,
278
  eval_strategy="steps",
279
- eval_steps=100,
280
  eval_accumulation_steps=1,
281
- save_strategy="no", # no intermediate checkpoints - only saved at the end (step 9)
 
 
282
  load_best_model_at_end=False,
283
- fp16=False, # CPU: no fp16
284
- bf16=False, # leave off unless your CPU supports it well
285
  report_to="none",
286
- dataloader_num_workers=0, # safest default on Windows
287
  remove_unused_columns=False,
288
- optim="adamw_torch", # CPU-safe optimizer (no paged_adamw_8bit)
289
- push_to_hub=False, # offline - nothing gets uploaded
290
- use_cpu=True,
291
  )
292
 
293
  trainer = Trainer(
@@ -298,72 +292,72 @@ trainer = Trainer(
298
  data_collator=custom_completion_collator,
299
  )
300
 
301
- # ============================================================
302
- # 8. TRAIN (resumes automatically if checkpoints exist)
303
- # ============================================================
304
  print("\n" + "=" * 60)
305
- print("Starting training...")
306
  print("=" * 60)
307
- print(f"Rows: {len(train_dataset):,} train / {len(val_dataset):,} val")
308
- print("CPU training is slow - this may take a while depending on MAX_ROWS.")
309
- print("Note: no intermediate checkpoints are saved - if this run is")
310
- print("interrupted, training must be restarted from scratch.")
311
 
312
- trainer.train()
 
 
 
 
 
 
 
 
 
 
 
 
 
313
 
314
- # ============================================================
315
- # 9. SAVE LoRA ADAPTER
316
- # ============================================================
317
- adapter_path = os.path.join(OUTPUT_DIR, "final_adapter")
318
- model.save_pretrained(adapter_path)
319
- tokenizer.save_pretrained(adapter_path)
320
- print(f"\nTraining complete. LoRA adapter saved to: {adapter_path}")
321
 
322
  # ============================================================
323
- # 10. MERGE ADAPTER INTO BASE MODEL (produces a standalone model)
324
  # ============================================================
325
  print("\n" + "=" * 60)
326
- print("Merging LoRA adapter into base model...")
327
  print("=" * 60)
328
 
329
- del model, trainer
 
330
  gc.collect()
 
331
 
 
332
  base_model = AutoModelForCausalLM.from_pretrained(
333
- MODEL_PATH,
334
- torch_dtype=torch.bfloat16,
 
335
  trust_remote_code=True,
 
336
  )
337
 
338
- merged_model = PeftModel.from_pretrained(base_model, adapter_path)
 
339
  merged_model = merged_model.merge_and_unload()
340
 
341
- merged_save_dir = os.path.join(OUTPUT_DIR, "merged_model")
342
- merged_model.save_pretrained(merged_save_dir)
343
- tokenizer.save_pretrained(merged_save_dir)
344
- print(f"Merged model saved to: {merged_save_dir}")
 
 
345
 
346
  # ============================================================
347
- # 11. QUICK SANITY TEST
348
  # ============================================================
349
- print("\n" + "=" * 60)
350
- print("Running quick sanity test on merged model...")
351
- print("=" * 60)
352
-
353
-
354
- def transliterate(urdu_text, max_new_tokens=128):
355
- prompt_text = f"""### Instruction:
356
- Transliterate the following Urdu text into Roman Urdu.
357
- Output ONLY the Roman Urdu. No translation. No explanation.
358
-
359
- ### Input:
360
- {urdu_text}
361
-
362
- ### Response:
363
- """
364
- inputs = tokenizer(prompt_text, return_tensors="pt")
365
  with torch.no_grad():
366
- outputs = merged_model.generate(
367
  **inputs,
368
  max_new_tokens=max_new_tokens,
369
  do_sample=False,
@@ -373,15 +367,15 @@ Output ONLY the Roman Urdu. No translation. No explanation.
373
  return tokenizer.decode(generated_ids, skip_special_tokens=True).strip()
374
 
375
 
376
- # Pull a couple of examples straight from your own val set for the test
377
- sample_rows = val_df.head(3) if len(val_df) > 0 else train_df.head(3)
378
- for _, row in sample_rows.iterrows():
379
- result = transliterate(row[URDU_COL])
380
- print(f"Urdu : {row[URDU_COL]}")
381
- print(f"Expected : {row[ROMAN_COL]}")
382
- print(f"Predicted: {result}")
383
- print("-" * 70)
384
-
385
- print("\nDone. Model artifacts:")
386
- print(f" LoRA adapter : {adapter_path}")
387
- print(f" Merged model : {merged_save_dir}")
 
1
+ #!/usr/bin/env python3
2
  """
3
+ HBL GPU Server - Stage 2 Fine-tuning Script
4
+ ============================================
5
+ Further fine-tunes your existing Urdu -> Roman Urdu transliteration model
6
+ using ONLY pairs_clean.csv (real NADRA log data), then merges the new
7
+ LoRA adapter into your existing fine-tuned model and saves a brand new
8
+ merged model locally.
9
+
10
+ FULLY OFFLINE - no internet / Hugging Face Hub access is used anywhere.
11
+ Install packages on a machine with wifi, transfer the wheel files here,
12
+ then `pip install --no-index --find-links=<folder> -r requirements.txt`.
13
+ See requirements.txt for the package list and offline-install notes.
 
 
 
 
 
 
 
14
  """
15
 
16
  import os
17
 
18
+ # --- Hard block any accidental network calls (safe on an air-gapped box) ---
 
 
19
  os.environ["HF_HUB_OFFLINE"] = "1"
20
  os.environ["TRANSFORMERS_OFFLINE"] = "1"
21
+ os.environ["CUDA_VISIBLE_DEVICES"] = "0"
22
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
23
 
24
  import gc
25
  import re
 
26
  import unicodedata
27
  import pandas as pd
28
  import torch
29
+ from sklearn.model_selection import train_test_split
30
  from datasets import Dataset
31
  from transformers import (
32
  AutoTokenizer,
 
34
  TrainingArguments,
35
  Trainer,
36
  )
37
+ from peft import (
38
+ LoraConfig,
39
+ get_peft_model,
40
+ PeftModel,
41
+ )
42
 
43
  # ============================================================
44
+ # CONFIG - EDIT THESE FOR YOUR HBL GPU SERVER
45
  # ============================================================
46
 
47
+ # !!! IMPORTANT - READ THIS !!!
48
+ # Your currently-deployed model is saved as a .gguf (Ollama/llama.cpp format).
49
+ # GGUF is an inference-only quantized format - it CANNOT be loaded by
50
+ # transformers/PEFT to continue fine-tuning it. You need the ORIGINAL
51
+ # merged HF/safetensors folder instead - i.e. the folder that came out of
52
+ # the "merge_and_unload()" step in your Kaggle notebook (Cell 9), before
53
+ # it was converted to GGUF for Ollama. Copy that folder onto this server
54
+ # and point BASE_MODEL_PATH at it.
55
+ BASE_MODEL_PATH = "/path/to/your/merged_model" # <-- SET THIS (NOT a .gguf file)
56
+
57
+ # Path to your HBL data. CSV must have columns named exactly: urdu, roman
58
+ CSV_PATH = "./pairs_clean.csv"
59
+
60
+ # ADJUST THIS: pairs_clean.csv has ~137k rows total.
61
+ # Set N_ROWS to an integer (e.g. 40000) to train on a random subset,
62
+ # or set to None to use the full ~137k rows.
63
+ N_ROWS = None # <-- ADJUST THIS
64
+
65
+ # Fraction of the (possibly subsetted) data held out for validation
66
+ VAL_FRACTION = 0.02
67
+
68
+ # Where training checkpoints + the new LoRA adapter are saved
69
+ OUTPUT_DIR = "./hbl-stage2-finetune"
70
+ ADAPTER_DIR = f"{OUTPUT_DIR}/final_adapter"
71
+
72
+ # Where the FINAL merged model (your old model + new adapter, merged) is saved
73
+ MERGED_MODEL_DIR = f"{OUTPUT_DIR}/merged_model_v2"
74
+
75
+ MAX_SEQ_LEN = 224
76
+
77
+ # Training hyperparameters (A16, ~16GB VRAM, bf16 no quantization -
78
+ # if you hit OOM, lower PER_DEVICE_BATCH_SIZE first, e.g. to 2, and raise GRAD_ACCUM_STEPS to compensate)
79
+ PER_DEVICE_BATCH_SIZE = 4
80
+ GRAD_ACCUM_STEPS = 4
81
  NUM_EPOCHS = 1
 
 
82
  LEARNING_RATE = 2e-4
83
+ WARMUP_STEPS = 200
84
+ SAVE_STEPS = 500
85
+ EVAL_STEPS = 500
86
+
87
+ RESPONSE_TEMPLATE = "### Response:\n"
88
+
89
 
90
  # ============================================================
91
+ # STEP 1: LOAD + CLEAN pairs_clean.csv
92
  # ============================================================
93
  print("=" * 60)
94
+ print("Loading pairs_clean.csv")
95
  print("=" * 60)
96
 
97
+ df = pd.read_csv(CSV_PATH)
98
+ df = df.rename(columns={c: c.strip().lower() for c in df.columns})
99
+ assert "urdu" in df.columns and "roman" in df.columns, (
100
+ f"Expected columns 'urdu' and 'roman', got: {list(df.columns)}"
101
+ )
102
+ print(f"Loaded {len(df):,} total rows from {CSV_PATH}")
103
 
104
+ # Shuffle before subsetting so the sample isn't biased by CSV ordering
105
+ df = df.sample(frac=1, random_state=42).reset_index(drop=True)
106
 
107
+ if N_ROWS is not None:
108
+ df = df.iloc[:N_ROWS].reset_index(drop=True)
109
+ print(f"Using a subset of {len(df):,} rows (N_ROWS={N_ROWS})")
110
  else:
111
+ print(f"Using all {len(df):,} rows")
 
 
 
 
 
 
112
 
113
 
114
  def clean_urdu(s):
 
121
 
122
 
123
  def clean_roman(s):
124
+ # Uppercased to match the casing convention of your existing model
125
  if not isinstance(s, str) or len(s.strip()) == 0:
126
  return ""
127
  s = unicodedata.normalize("NFC", s)
 
132
  return s.upper()
133
 
134
 
135
+ df["urdu"] = df["urdu"].apply(clean_urdu)
136
+ df["roman"] = df["roman"].apply(clean_roman)
137
+ df = df[(df["urdu"].str.len() > 0) & (df["roman"].str.len() > 0)]
138
+ df = df.drop_duplicates(subset=["urdu", "roman"])
139
+ df = df[df["urdu"].str.len() <= 200]
140
+ df = df[df["roman"].str.len() <= 250]
141
+ df = df.reset_index(drop=True)
142
+ print(f"After cleaning: {len(df):,} rows")
 
143
 
144
+ train_df, val_df = train_test_split(df, test_size=VAL_FRACTION, random_state=42)
145
+ print(f"Train: {len(train_df):,} | Val: {len(val_df):,}")
146
 
 
 
147
 
148
+ # ============================================================
149
+ # STEP 2: LOAD TOKENIZER (local only, from your existing model)
150
+ # ============================================================
151
+ print("\nLoading tokenizer from local model folder...")
152
+ tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_PATH, local_files_only=True)
153
+ tokenizer.pad_token = tokenizer.eos_token
154
+ tokenizer.padding_side = "right"
155
+
156
+
157
+ def format_example(urdu, roman):
158
+ return (
159
+ "### Instruction:\n"
160
+ "Transliterate the following Urdu text into Roman Urdu. \n"
161
+ "Output ONLY the Roman Urdu. No translation. No explanation.\n\n"
162
+ "### Input:\n"
163
+ f"{urdu}\n\n"
164
+ f"{RESPONSE_TEMPLATE}{roman}{tokenizer.eos_token}"
165
+ )
166
+
167
+
168
+ train_texts = [format_example(u, r) for u, r in zip(train_df["urdu"], train_df["roman"])]
169
+ val_texts = [format_example(u, r) for u, r in zip(val_df["urdu"], val_df["roman"])]
170
+
171
+ train_dataset = Dataset.from_dict({"text": train_texts})
172
+ val_dataset = Dataset.from_dict({"text": val_texts})
173
+
174
+ print("\nSample formatted example:")
175
+ print(train_texts[0])
176
 
 
 
 
 
177
 
178
  # ============================================================
179
+ # STEP 3: LOAD YOUR EXISTING FINE-TUNED MODEL (4-bit, for training)
180
  # ============================================================
181
  print("\n" + "=" * 60)
182
+ print("Loading base model (your existing fine-tuned model) in bf16")
183
  print("=" * 60)
184
 
185
+ gc.collect()
186
+ torch.cuda.empty_cache()
 
187
 
188
+ # No bitsandbytes / quantization - the model fits fine in bf16 on 16GB VRAM
189
+ # since only the small LoRA adapter needs gradients (base weights are frozen).
190
  model = AutoModelForCausalLM.from_pretrained(
191
+ BASE_MODEL_PATH,
192
+ dtype=torch.bfloat16,
193
+ device_map={"": 0},
194
  trust_remote_code=True,
195
+ local_files_only=True,
196
  )
197
  model.config.use_cache = False
198
+ model.gradient_checkpointing_enable()
199
+ model.enable_input_require_grads() # required so gradients flow into the LoRA adapter
200
+ # with the base model frozen + gradient checkpointing enabled
201
 
 
 
 
202
  lora_config = LoraConfig(
203
+ r=32,
204
+ lora_alpha=64,
205
  target_modules=[
206
+ "q_proj", "k_proj", "v_proj", "o_proj",
207
+ "gate_proj", "up_proj", "down_proj",
 
 
 
 
 
208
  ],
209
  lora_dropout=0.05,
210
  bias="none",
 
212
  )
213
 
214
  model = get_peft_model(model, lora_config)
215
+ print("Model loaded and LoRA adapter attached.")
216
  model.print_trainable_parameters()
217
 
218
+
219
  # ============================================================
220
+ # STEP 4: TOKENIZE + CUSTOM COMPLETION-ONLY COLLATOR
221
  # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  def tokenize_function(examples):
223
  return tokenizer(
224
  examples["text"],
225
  truncation=True,
226
+ max_length=MAX_SEQ_LEN,
227
  padding=False,
228
  return_tensors=None,
229
  )
 
232
  tokenized_train = train_dataset.map(tokenize_function, batched=True, remove_columns=["text"])
233
  tokenized_val = val_dataset.map(tokenize_function, batched=True, remove_columns=["text"])
234
 
235
+ model.config.use_cache = False
236
+ response_ids = tokenizer.encode(RESPONSE_TEMPLATE, add_special_tokens=False)
 
 
 
237
 
238
 
239
  def custom_completion_collator(features):
240
  batch = tokenizer.pad(features, return_tensors="pt")
241
  labels = batch["input_ids"].clone()
 
 
242
  labels[batch["attention_mask"] == 0] = -100
243
 
 
244
  window_size = len(response_ids)
245
  for i in range(labels.shape[0]):
246
  label_seq = labels[i].tolist()
 
257
 
258
 
259
  # ============================================================
260
+ # STEP 5: TRAIN
261
  # ============================================================
262
  training_args = TrainingArguments(
263
  output_dir=OUTPUT_DIR,
 
266
  gradient_accumulation_steps=GRAD_ACCUM_STEPS,
267
  num_train_epochs=NUM_EPOCHS,
268
  learning_rate=LEARNING_RATE,
269
+ warmup_steps=WARMUP_STEPS,
270
+ logging_steps=10,
271
  eval_strategy="steps",
272
+ eval_steps=EVAL_STEPS,
273
  eval_accumulation_steps=1,
274
+ save_strategy="steps",
275
+ save_steps=SAVE_STEPS,
276
+ save_total_limit=2,
277
  load_best_model_at_end=False,
278
+ bf16=True,
279
+ fp16=False,
280
  report_to="none",
281
+ dataloader_num_workers=2,
282
  remove_unused_columns=False,
283
+ optim="adamw_torch", # standard optimizer, no bitsandbytes dependency
284
+ push_to_hub=False,
 
285
  )
286
 
287
  trainer = Trainer(
 
292
  data_collator=custom_completion_collator,
293
  )
294
 
 
 
 
295
  print("\n" + "=" * 60)
296
+ print("Starting training")
297
  print("=" * 60)
 
 
 
 
298
 
299
+ checkpoints = (
300
+ [d for d in os.listdir(OUTPUT_DIR) if d.startswith("checkpoint-")]
301
+ if os.path.exists(OUTPUT_DIR) else []
302
+ )
303
+ if checkpoints:
304
+ print(f"Found {len(checkpoints)} existing checkpoints. Resuming...")
305
+ trainer.train(resume_from_checkpoint=True)
306
+ else:
307
+ print("No checkpoints found. Starting fresh training...")
308
+ trainer.train()
309
+
310
+ model.save_pretrained(ADAPTER_DIR)
311
+ tokenizer.save_pretrained(ADAPTER_DIR)
312
+ print(f"\nStage-2 LoRA adapter saved to: {ADAPTER_DIR}")
313
 
 
 
 
 
 
 
 
314
 
315
  # ============================================================
316
+ # STEP 6: MERGE NEW ADAPTER INTO YOUR EXISTING MODEL
317
  # ============================================================
318
  print("\n" + "=" * 60)
319
+ print("Merging new adapter into your existing fine-tuned model")
320
  print("=" * 60)
321
 
322
+ del trainer
323
+ del model
324
  gc.collect()
325
+ torch.cuda.empty_cache()
326
 
327
+ print("Loading base model (full precision, CPU) for a clean merge...")
328
  base_model = AutoModelForCausalLM.from_pretrained(
329
+ BASE_MODEL_PATH,
330
+ dtype=torch.bfloat16,
331
+ device_map="cpu",
332
  trust_remote_code=True,
333
+ local_files_only=True,
334
  )
335
 
336
+ print("Merging weights...")
337
+ merged_model = PeftModel.from_pretrained(base_model, ADAPTER_DIR)
338
  merged_model = merged_model.merge_and_unload()
339
 
340
+ os.makedirs(MERGED_MODEL_DIR, exist_ok=True)
341
+ merged_model.save_pretrained(MERGED_MODEL_DIR)
342
+ tokenizer.save_pretrained(MERGED_MODEL_DIR)
343
+ print(f"\nFinal merged model saved to: {MERGED_MODEL_DIR}")
344
+ print("You can now convert this folder to GGUF for Ollama, same as before.")
345
+
346
 
347
  # ============================================================
348
+ # STEP 7 (OPTIONAL): QUICK SANITY CHECK
349
  # ============================================================
350
+ def transliterate(model_for_test, urdu_text, max_new_tokens=128):
351
+ prompt_text = (
352
+ "### Instruction:\n"
353
+ "Transliterate the following Urdu text into Roman Urdu. \n"
354
+ "Output ONLY the Roman Urdu. No translation. No explanation.\n\n"
355
+ "### Input:\n"
356
+ f"{urdu_text}\n\n{RESPONSE_TEMPLATE}"
357
+ )
358
+ inputs = tokenizer(prompt_text, return_tensors="pt").to(model_for_test.device)
 
 
 
 
 
 
 
359
  with torch.no_grad():
360
+ outputs = model_for_test.generate(
361
  **inputs,
362
  max_new_tokens=max_new_tokens,
363
  do_sample=False,
 
367
  return tokenizer.decode(generated_ids, skip_special_tokens=True).strip()
368
 
369
 
370
+ if __name__ == "__main__":
371
+ print("\n" + "=" * 60)
372
+ print("Running a quick sanity check on the merged model")
373
+ print("=" * 60)
374
+ test_model = merged_model.to("cuda") if torch.cuda.is_available() else merged_model
375
+ sample_rows = df.sample(min(5, len(df)), random_state=1)
376
+ for _, row in sample_rows.iterrows():
377
+ result = transliterate(test_model, row["urdu"])
378
+ print(f"Urdu : {row['urdu']}")
379
+ print(f"Roman (expected) : {row['roman']}")
380
+ print(f"Roman (model) : {result}")
381
+ print("-" * 70)