ogx786 commited on
Commit
cf752da
·
verified ·
1 Parent(s): 82823df

Update latest_finetune.py

Browse files
Files changed (1) hide show
  1. latest_finetune.py +84 -45
latest_finetune.py CHANGED
@@ -1,19 +1,27 @@
1
  """
2
  ================================================================================
3
- Tiny-Aya v3 -> v4 Sequential Fine-Tuning: NADRA/HBL Structured Transliteration
4
  ================================================================================
5
 
6
  Purpose
7
  -------
8
- Continue fine-tuning the already-transliteration-tuned v3 checkpoint
9
- (CohereLabs/tiny-aya-global + merged LoRA, saved as a full HF model) on a
10
  narrower, structured task: NADRA/HBL-style pipe-separated Urdu records
11
  (name | father/husband name | guardian name | address | address | city,district)
12
  -> the equivalent Roman Urdu record, preserving field order and separators
13
  EXACTLY.
14
 
15
- This is NOT resumed from the old v3 LoRA adapter (that adapter is already
16
- merged into the v3 weights). Instead: v3's merged weights are frozen and
 
 
 
 
 
 
 
 
17
  quantized to 4-bit, and a FRESH LoRA adapter is trained on top of them for
18
  this specific task. That is the correct pattern for "continue fine-tuning
19
  a merged checkpoint."
@@ -34,9 +42,11 @@ How to use
34
  and run once to see real token-length stats for your NADRA records,
35
  then set MAX_LENGTH accordingly and set this back to False.
36
  3. Run: python finetune_v4_nadra.py
37
- 4. Adapter (not merged) is saved to OUTPUT_DIR/final_adapter.
38
- 5. Run merge_adapter() (bottom of this file, or call as a separate step)
39
- to produce a merged, standalone model for deployment.
 
 
40
  """
41
 
42
  import os
@@ -71,9 +81,12 @@ from peft import (
71
  # ==============================================================================
72
 
73
  # --- Paths -------------------------------------------------------------------
74
- # Local path to your v3 checkpoint (merged model.safetensors + config.json +
75
  # tokenizer files + generation_config.json + chat_template.jinja).
76
- V3_MODEL_PATH = r"./tiny-aya-v3-merged"
 
 
 
77
 
78
  # Folder containing splits/train.csv, splits/val.csv, splits/test.csv
79
  DATA_DIR = r"./splits"
@@ -103,11 +116,15 @@ MAX_LENGTH = 384 # starting default; confirm/adjust using the analysis pass
103
  SEED = 42
104
 
105
  # --- LoRA config ---------------------------------------------------------------
106
- # Rank/alpha kept from v3 (higher capacity helps structural fidelity).
107
- # Dropout reduced vs. v3 (0.05 -> 0.02): 2.9M rows makes overfitting on one
108
- # epoch unlikely, and less dropout helps convergence on a narrower task.
109
- LORA_R = 32
110
- LORA_ALPHA = 64
 
 
 
 
111
  LORA_DROPOUT = 0.02
112
  LORA_TARGET_MODULES = [
113
  "q_proj", "k_proj", "v_proj", "o_proj",
@@ -115,16 +132,35 @@ LORA_TARGET_MODULES = [
115
  ]
116
 
117
  # --- Training hyperparameters ---------------------------------------------------
118
- # LR lowered vs. v3 (2e-4 -> 1e-4): this is a refinement pass on an already
119
- # fine-tuned checkpoint, not learning a domain from scratch. A gentler LR
120
- # reduces the risk of degrading what v3 already learned.
121
- LEARNING_RATE = 1e-4
 
 
 
122
  NUM_TRAIN_EPOCHS = 1 # with ~2.9M rows, one epoch is already a large amount
123
- # of gradient updates on a fairly repetitive structure.
124
- # Bump only after checking eval loss on a pilot run.
125
- PER_DEVICE_TRAIN_BATCH_SIZE = 4
126
- PER_DEVICE_EVAL_BATCH_SIZE = 4
127
- GRADIENT_ACCUMULATION_STEPS = 4 # effective batch size = 16 (same as v3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  WARMUP_RATIO = 0.03 # ratio, not fixed steps -- scales with LIMIT_ROWS
129
 
130
  LOGGING_STEPS = 20
@@ -140,7 +176,7 @@ USE_BNB_PAGED_OPTIMIZER = False
140
 
141
  # --- Merging ---------------------------------------------------------------
142
  # If True, the script merges the trained LoRA adapter into a fresh copy of
143
- # the v3 base model and saves a standalone deployable model immediately
144
  # after training finishes, in the same process/run. Set False if you'd
145
  # rather train now and merge later as a separate step (e.g. to free up the
146
  # GPU sooner, or to merge from a specific earlier checkpoint by hand).
@@ -255,7 +291,7 @@ def format_example(urdu: str, roman: str, eos_token: str) -> str:
255
 
256
 
257
  # ==============================================================================
258
- # 4. Completion-only masking (offset-mapping based, same approach as v3)
259
  # ==============================================================================
260
  def build_tokenize_fn(tokenizer, max_length):
261
  def tokenize_function(examples):
@@ -324,13 +360,13 @@ def analyze_lengths(tokenizer, texts, sample_size):
324
 
325
 
326
  # ==============================================================================
327
- # 6. Model loading: v3 (local, merged) -> 4-bit quantized + fresh LoRA adapter
328
  # ==============================================================================
329
  def load_model_and_tokenizer():
330
- if not os.path.isdir(V3_MODEL_PATH):
331
  raise FileNotFoundError(
332
- f"V3_MODEL_PATH does not exist: {V3_MODEL_PATH}. "
333
- f"This must be a local directory containing the v3 merged "
334
  f"checkpoint (model.safetensors, config.json, tokenizer files)."
335
  )
336
 
@@ -341,9 +377,9 @@ def load_model_and_tokenizer():
341
  bnb_4bit_use_double_quant=True,
342
  )
343
 
344
- print(f"Loading v3 checkpoint from local path: {V3_MODEL_PATH}")
345
  model = AutoModelForCausalLM.from_pretrained(
346
- V3_MODEL_PATH,
347
  quantization_config=bnb_config,
348
  torch_dtype=torch.float16,
349
  device_map={"": 0},
@@ -352,10 +388,10 @@ def load_model_and_tokenizer():
352
  )
353
  model.config.use_cache = False
354
  model = prepare_model_for_kbit_training(model)
355
- model.gradient_checkpointing_enable()
356
 
357
  tokenizer = AutoTokenizer.from_pretrained(
358
- V3_MODEL_PATH, local_files_only=True
359
  )
360
  tokenizer.pad_token = tokenizer.eos_token
361
  tokenizer.padding_side = "right"
@@ -396,7 +432,7 @@ def main():
396
  # --- Tokenizer needed for both the length-analysis path and formatting ---
397
  # Loaded here (unquantized/tokenizer-only) so ANALYZE_LENGTHS_ONLY can run
398
  # without paying the cost of loading the full 4-bit model.
399
- tokenizer = AutoTokenizer.from_pretrained(V3_MODEL_PATH, local_files_only=True)
400
  tokenizer.pad_token = tokenizer.eos_token
401
 
402
  train_texts = [
@@ -440,7 +476,7 @@ def main():
440
  "before starting an unattended run."
441
  )
442
 
443
- # --- Load model (v3 -> 4-bit -> fresh LoRA) ---
444
  model, model_tokenizer = load_model_and_tokenizer()
445
  # tokenizer used for data collation must match the one used for tokenizing
446
  assert model_tokenizer.get_vocab() == tokenizer.get_vocab(), (
@@ -472,7 +508,10 @@ def main():
472
  remove_unused_columns=False,
473
  optim=optim_name,
474
  gradient_checkpointing=True,
475
- gradient_checkpointing_kwargs={"use_reentrant": True},
 
 
 
476
  # No push_to_hub / hub_model_id / hub_token: server has no internet.
477
  )
478
 
@@ -518,7 +557,7 @@ def main():
518
  print(f"Training complete. LoRA adapter saved to: {ADAPTER_DIR}")
519
 
520
  # --- Free the 4-bit training model's GPU memory before merging ---
521
- # Merging loads a fresh, unquantized copy of v3 on CPU, but the trainer
522
  # and quantized model from this run are still holding CUDA memory/cache
523
  # at this point. Explicitly drop references and clear the allocator
524
  # cache so the merge step (or anything run after it in this process)
@@ -529,7 +568,7 @@ def main():
529
  torch.cuda.empty_cache()
530
 
531
  if AUTO_MERGE_AFTER_TRAINING:
532
- print("Proceeding to merge the trained adapter into v3 (same run)...")
533
  merge_adapter()
534
  else:
535
  print(
@@ -539,7 +578,7 @@ def main():
539
 
540
 
541
  # ==============================================================================
542
- # 8. Merge LoRA adapter into v3 base -> standalone v4 model for deployment
543
  # ==============================================================================
544
  def merge_adapter():
545
  """
@@ -548,12 +587,12 @@ def merge_adapter():
548
  trained with AUTO_MERGE_AFTER_TRAINING = False, or want to re-merge from
549
  a specific checkpoint.
550
 
551
- Loads v3 in fp16 (not quantized) on CPU, merges the trained LoRA adapter
552
  into it, and saves a standalone model that can be loaded directly with
553
  AutoModelForCausalLM.from_pretrained() at deployment time -- no PEFT or
554
  bitsandbytes required for inference.
555
  """
556
- # Priority order for adapter source, mirroring v3's approach:
557
  adapter_path = None
558
  if os.path.exists(os.path.join(ADAPTER_DIR, "adapter_config.json")):
559
  adapter_path = ADAPTER_DIR
@@ -585,15 +624,15 @@ def merge_adapter():
585
  gc.collect()
586
  torch.cuda.empty_cache()
587
 
588
- print(f"Loading v3 base model (unquantized, CPU) from: {V3_MODEL_PATH}")
589
  base_model = AutoModelForCausalLM.from_pretrained(
590
- V3_MODEL_PATH,
591
  torch_dtype=torch.float16,
592
  device_map="cpu",
593
  trust_remote_code=True,
594
  local_files_only=True,
595
  )
596
- tokenizer = AutoTokenizer.from_pretrained(V3_MODEL_PATH, local_files_only=True)
597
 
598
  print("Merging LoRA weights into base...")
599
  merged_model = PeftModel.from_pretrained(base_model, adapter_path)
 
1
  """
2
  ================================================================================
3
+ Tiny-Aya v2 -> v4 Sequential Fine-Tuning: NADRA/HBL Structured Transliteration
4
  ================================================================================
5
 
6
  Purpose
7
  -------
8
+ Continue fine-tuning the v2 checkpoint (CohereLabs/tiny-aya-global + LoRA
9
+ trained on ~50K synthetic Urdu-Roman name/address pairs, merged) on a
10
  narrower, structured task: NADRA/HBL-style pipe-separated Urdu records
11
  (name | father/husband name | guardian name | address | address | city,district)
12
  -> the equivalent Roman Urdu record, preserving field order and separators
13
  EXACTLY.
14
 
15
+ Note: v2 is being used here (instead of the better-performing v3) purely
16
+ for practical reasons -- it's already on the HBL GPU server, whereas v3's
17
+ ~6.2GB model.safetensors would take a long time to transfer over a slow
18
+ connection. Because v2's baseline transliteration quality is weaker (only
19
+ 50K synthetic examples, output was "not that good"), this run is doing more
20
+ substantial learning than a pure refinement pass would -- see the LORA_R/
21
+ LORA_ALPHA and LEARNING_RATE comments below for how that changes things.
22
+
23
+ This is NOT resumed from the old v2 LoRA adapter (that adapter is already
24
+ merged into the v2 weights). Instead: v2's merged weights are frozen and
25
  quantized to 4-bit, and a FRESH LoRA adapter is trained on top of them for
26
  this specific task. That is the correct pattern for "continue fine-tuning
27
  a merged checkpoint."
 
42
  and run once to see real token-length stats for your NADRA records,
43
  then set MAX_LENGTH accordingly and set this back to False.
44
  3. Run: python finetune_v4_nadra.py
45
+ This trains, saves the LoRA adapter to OUTPUT_DIR/final_adapter, then
46
+ (by default, AUTO_MERGE_AFTER_TRAINING = True) merges it into v2 and
47
+ saves a standalone deployable model to OUTPUT_DIR/merged_model, all in
48
+ this one run. Set AUTO_MERGE_AFTER_TRAINING = False if you'd rather
49
+ merge later as a separate step instead.
50
  """
51
 
52
  import os
 
81
  # ==============================================================================
82
 
83
  # --- Paths -------------------------------------------------------------------
84
+ # Local path to your v2 checkpoint (merged model.safetensors + config.json +
85
  # tokenizer files + generation_config.json + chat_template.jinja).
86
+ # Local path to your v2 checkpoint on the HBL server (merged
87
+ # model.safetensors + config.json + tokenizer files + generation_config.json
88
+ # + chat_template.jinja). Point this at wherever v2 already lives locally.
89
+ V2_MODEL_PATH = r"./tiny-aya-v2-merged"
90
 
91
  # Folder containing splits/train.csv, splits/val.csv, splits/test.csv
92
  DATA_DIR = r"./splits"
 
116
  SEED = 42
117
 
118
  # --- LoRA config ---------------------------------------------------------------
119
+ # Rank/alpha bumped up vs. the original run (32/64 -> 48/96): v2 was only
120
+ # trained on 50K synthetic name/address pairs and its output quality was
121
+ # admittedly weak. This adapter now has to do more of the real learning
122
+ # (both phonetic mapping AND the NADRA structural constraints) rather than
123
+ # just refining an already-solid base, so it gets more capacity.
124
+ # Dropout kept low (0.02): 2.9M rows makes overfitting on one epoch unlikely
125
+ # regardless of starting checkpoint quality.
126
+ LORA_R = 48
127
+ LORA_ALPHA = 96
128
  LORA_DROPOUT = 0.02
129
  LORA_TARGET_MODULES = [
130
  "q_proj", "k_proj", "v_proj", "o_proj",
 
132
  ]
133
 
134
  # --- Training hyperparameters ---------------------------------------------------
135
+ # LR raised back up vs. a "gentle refinement" rate (1e-4 -> 1.8e-4): v2 only
136
+ # saw 50K synthetic examples and its transliteration quality was weak, so
137
+ # there's less already-good behavior worth protecting with a low LR. This
138
+ # run needs to do real, substantial learning (phonetic mapping AND NADRA
139
+ # structure), not just nudge an already-capable model -- closer to the
140
+ # original from-scratch rate (2e-4) than a fine-refinement rate.
141
+ LEARNING_RATE = 1.8e-4
142
  NUM_TRAIN_EPOCHS = 1 # with ~2.9M rows, one epoch is already a large amount
143
+ # of gradient updates. Given v2's weaker starting
144
+ # point, watch the eval loss curve from a LIMIT_ROWS
145
+ # pilot run -- if it's still improving steadily at
146
+ # the end of the epoch, a 2nd epoch is worth the
147
+ # extra compute time here more than it would have
148
+ # been for a v3-based refinement pass.
149
+ PER_DEVICE_TRAIN_BATCH_SIZE = 2 # kept small deliberately -- see note below
150
+ PER_DEVICE_EVAL_BATCH_SIZE = 2
151
+ GRADIENT_ACCUMULATION_STEPS = 8 # effective batch size = 16 (unchanged)
152
+ # Batch size lowered vs. the original (4 -> 2) and accumulation raised
153
+ # (4 -> 8) to keep the SAME effective batch of 16 while cutting per-step
154
+ # activation memory roughly in half. Two things increased memory pressure
155
+ # vs. the original v3-refinement config: MAX_LENGTH went up (224 -> 384,
156
+ # ~1.7x longer sequences) and LORA_R doubled (32 -> 48, more adapter
157
+ # activations at every targeted layer). The A16 vGPU slice here is a fixed
158
+ # 16GB budget with no headroom to discover through trial and error mid-run,
159
+ # so this trades a bit of throughput for a solid safety margin. If a
160
+ # LIMIT_ROWS pilot run shows plenty of free VRAM (check with `nvidia-smi`
161
+ # while it's training), PER_DEVICE_TRAIN_BATCH_SIZE can be raised back
162
+ # toward 4 (halve GRADIENT_ACCUMULATION_STEPS to match, keeping effective
163
+ # batch at 16).
164
  WARMUP_RATIO = 0.03 # ratio, not fixed steps -- scales with LIMIT_ROWS
165
 
166
  LOGGING_STEPS = 20
 
176
 
177
  # --- Merging ---------------------------------------------------------------
178
  # If True, the script merges the trained LoRA adapter into a fresh copy of
179
+ # the v2 base model and saves a standalone deployable model immediately
180
  # after training finishes, in the same process/run. Set False if you'd
181
  # rather train now and merge later as a separate step (e.g. to free up the
182
  # GPU sooner, or to merge from a specific earlier checkpoint by hand).
 
291
 
292
 
293
  # ==============================================================================
294
+ # 4. Completion-only masking (offset-mapping based, same approach as v2)
295
  # ==============================================================================
296
  def build_tokenize_fn(tokenizer, max_length):
297
  def tokenize_function(examples):
 
360
 
361
 
362
  # ==============================================================================
363
+ # 6. Model loading: v2 (local, merged) -> 4-bit quantized + fresh LoRA adapter
364
  # ==============================================================================
365
  def load_model_and_tokenizer():
366
+ if not os.path.isdir(V2_MODEL_PATH):
367
  raise FileNotFoundError(
368
+ f"V2_MODEL_PATH does not exist: {V2_MODEL_PATH}. "
369
+ f"This must be a local directory containing the v2 merged "
370
  f"checkpoint (model.safetensors, config.json, tokenizer files)."
371
  )
372
 
 
377
  bnb_4bit_use_double_quant=True,
378
  )
379
 
380
+ print(f"Loading v2 checkpoint from local path: {V2_MODEL_PATH}")
381
  model = AutoModelForCausalLM.from_pretrained(
382
+ V2_MODEL_PATH,
383
  quantization_config=bnb_config,
384
  torch_dtype=torch.float16,
385
  device_map={"": 0},
 
388
  )
389
  model.config.use_cache = False
390
  model = prepare_model_for_kbit_training(model)
391
+ model.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
392
 
393
  tokenizer = AutoTokenizer.from_pretrained(
394
+ V2_MODEL_PATH, local_files_only=True
395
  )
396
  tokenizer.pad_token = tokenizer.eos_token
397
  tokenizer.padding_side = "right"
 
432
  # --- Tokenizer needed for both the length-analysis path and formatting ---
433
  # Loaded here (unquantized/tokenizer-only) so ANALYZE_LENGTHS_ONLY can run
434
  # without paying the cost of loading the full 4-bit model.
435
+ tokenizer = AutoTokenizer.from_pretrained(V2_MODEL_PATH, local_files_only=True)
436
  tokenizer.pad_token = tokenizer.eos_token
437
 
438
  train_texts = [
 
476
  "before starting an unattended run."
477
  )
478
 
479
+ # --- Load model (v2 -> 4-bit -> fresh LoRA) ---
480
  model, model_tokenizer = load_model_and_tokenizer()
481
  # tokenizer used for data collation must match the one used for tokenizing
482
  assert model_tokenizer.get_vocab() == tokenizer.get_vocab(), (
 
508
  remove_unused_columns=False,
509
  optim=optim_name,
510
  gradient_checkpointing=True,
511
+ gradient_checkpointing_kwargs={"use_reentrant": False}, # more memory-
512
+ # efficient than reentrant checkpointing, and the current recommended
513
+ # setting for PEFT + quantized models -- matters more here given the
514
+ # fixed 16GB A16 budget.
515
  # No push_to_hub / hub_model_id / hub_token: server has no internet.
516
  )
517
 
 
557
  print(f"Training complete. LoRA adapter saved to: {ADAPTER_DIR}")
558
 
559
  # --- Free the 4-bit training model's GPU memory before merging ---
560
+ # Merging loads a fresh, unquantized copy of v2 on CPU, but the trainer
561
  # and quantized model from this run are still holding CUDA memory/cache
562
  # at this point. Explicitly drop references and clear the allocator
563
  # cache so the merge step (or anything run after it in this process)
 
568
  torch.cuda.empty_cache()
569
 
570
  if AUTO_MERGE_AFTER_TRAINING:
571
+ print("Proceeding to merge the trained adapter into v2 (same run)...")
572
  merge_adapter()
573
  else:
574
  print(
 
578
 
579
 
580
  # ==============================================================================
581
+ # 8. Merge LoRA adapter into v2 base -> standalone v4 model for deployment
582
  # ==============================================================================
583
  def merge_adapter():
584
  """
 
587
  trained with AUTO_MERGE_AFTER_TRAINING = False, or want to re-merge from
588
  a specific checkpoint.
589
 
590
+ Loads v2 in fp16 (not quantized) on CPU, merges the trained LoRA adapter
591
  into it, and saves a standalone model that can be loaded directly with
592
  AutoModelForCausalLM.from_pretrained() at deployment time -- no PEFT or
593
  bitsandbytes required for inference.
594
  """
595
+ # Priority order for adapter source, mirroring v2's approach:
596
  adapter_path = None
597
  if os.path.exists(os.path.join(ADAPTER_DIR, "adapter_config.json")):
598
  adapter_path = ADAPTER_DIR
 
624
  gc.collect()
625
  torch.cuda.empty_cache()
626
 
627
+ print(f"Loading v2 base model (unquantized, CPU) from: {V2_MODEL_PATH}")
628
  base_model = AutoModelForCausalLM.from_pretrained(
629
+ V2_MODEL_PATH,
630
  torch_dtype=torch.float16,
631
  device_map="cpu",
632
  trust_remote_code=True,
633
  local_files_only=True,
634
  )
635
+ tokenizer = AutoTokenizer.from_pretrained(V2_MODEL_PATH, local_files_only=True)
636
 
637
  print("Merging LoRA weights into base...")
638
  merged_model = PeftModel.from_pretrained(base_model, adapter_path)