chrisjcc commited on
Commit
a0dfd87
·
verified ·
1 Parent(s): f3d0de1

This is inference not training demo app

Browse files
Files changed (2) hide show
  1. src/training/__init__.py +0 -0
  2. src/training/trainer.py +0 -806
src/training/__init__.py DELETED
File without changes
src/training/trainer.py DELETED
@@ -1,806 +0,0 @@
1
- """Training pipelines for SFT and DPO.
2
-
3
- This module contains the setup and execution logic for fine-tuning language models
4
- using Supervised Fine-Tuning and Direct Preference Optimization via the TRL library.
5
- """
6
-
7
- import ast
8
- import logging
9
- import os
10
- import re
11
- from typing import Any, Tuple
12
-
13
- try:
14
- import torch._inductor.config # noqa: F401
15
- import unsloth # noqa: F401
16
- except ImportError:
17
- pass
18
-
19
- import torch
20
- from datasets import load_dataset
21
- from omegaconf import DictConfig
22
- from peft import LoraConfig, get_peft_model
23
- from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
24
- from transformers.trainer_utils import get_last_checkpoint
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
-
29
- def load_model_and_tokenizer(
30
- model_cfg: DictConfig, is_train: bool = True
31
- ) -> Tuple[Any, Any]:
32
- """Load tokenizer and model with given configuration.
33
-
34
- Args:
35
- model_cfg (DictConfig): The Hydra configuration for the model.
36
- is_train (bool): Whether to prepare the model for training
37
- (e.g. enable gradients).
38
-
39
- Returns:
40
- Tuple[Any, Any]: A tuple containing the loaded (model, tokenizer).
41
- """
42
- logger.info(f"Loading model {model_cfg.name}...")
43
-
44
- # Optional Unsloth integration for high-performance memory-efficient training
45
- use_unsloth = model_cfg.get("use_unsloth", False)
46
- load_in_8bit = model_cfg.get("load_in_8bit", False)
47
- load_in_4bit = model_cfg.get("load_in_4bit", False)
48
-
49
- if use_unsloth:
50
- try:
51
- from unsloth import FastLanguageModel
52
-
53
- logger.info("Unsloth is enabled. Loading via FastLanguageModel...")
54
- model, tokenizer = FastLanguageModel.from_pretrained(
55
- model_name=model_cfg.name,
56
- max_seq_length=model_cfg.get("max_seq_length", 2048),
57
- dtype=(
58
- getattr(torch, model_cfg.torch_dtype)
59
- if hasattr(model_cfg, "torch_dtype")
60
- else None
61
- ),
62
- load_in_4bit=load_in_4bit,
63
- trust_remote_code=model_cfg.get("trust_remote_code", False),
64
- )
65
-
66
- if is_train and "lora" in model_cfg:
67
- lora_cfg = model_cfg.lora
68
- logger.info("Applying Unsloth optimized LoRA adapters...")
69
- model = FastLanguageModel.get_peft_model(
70
- model,
71
- r=lora_cfg.r,
72
- target_modules=list(lora_cfg.target_modules),
73
- lora_alpha=lora_cfg.lora_alpha,
74
- lora_dropout=lora_cfg.lora_dropout,
75
- bias=lora_cfg.bias,
76
- use_gradient_checkpointing="unsloth",
77
- random_state=3407,
78
- )
79
-
80
- return model, tokenizer
81
-
82
- except ImportError as e:
83
- logger.warning(
84
- f"unsloth import failed: {e}\n"
85
- "Falling back to native HuggingFace transformers."
86
- )
87
-
88
- # Native HuggingFace Fallback / Standard loading
89
- tokenizer = AutoTokenizer.from_pretrained(
90
- model_cfg.name, trust_remote_code=model_cfg.trust_remote_code
91
- )
92
-
93
- if tokenizer.pad_token is None:
94
- tokenizer.pad_token = tokenizer.eos_token
95
- tokenizer.padding_side = "right"
96
-
97
- kwargs = {
98
- "dtype": getattr(torch, model_cfg.torch_dtype),
99
- "trust_remote_code": model_cfg.trust_remote_code,
100
- }
101
-
102
- if torch.cuda.is_available():
103
- if model_cfg.device_map == "auto" and (load_in_8bit or load_in_4bit):
104
- from accelerate import Accelerator
105
-
106
- kwargs["device_map"] = {"": Accelerator().local_process_index}
107
- else:
108
- kwargs["device_map"] = model_cfg.device_map
109
-
110
- if load_in_8bit or load_in_4bit:
111
- compute_dtype = getattr(
112
- torch, model_cfg.get("bnb_4bit_compute_dtype", "bfloat16")
113
- )
114
-
115
- quantization_config = BitsAndBytesConfig(
116
- load_in_8bit=load_in_8bit,
117
- load_in_4bit=load_in_4bit,
118
- # QLoRA settings
119
- bnb_4bit_quant_type=model_cfg.get("bnb_4bit_quant_type", "nf4"),
120
- bnb_4bit_compute_dtype=compute_dtype,
121
- bnb_4bit_use_double_quant=model_cfg.get(
122
- "bnb_4bit_use_double_quant", True
123
- ),
124
- )
125
-
126
- kwargs["quantization_config"] = quantization_config
127
- else:
128
- # Prevent meta device offloading on Mac/CPU which crashes PEFT backward pass
129
- if torch.backends.mps.is_available():
130
- kwargs["device_map"] = {"": "mps"}
131
- else:
132
- kwargs["device_map"] = {"": "cpu"}
133
-
134
- if load_in_8bit or load_in_4bit:
135
- logger.warning(
136
- "CUDA is not available. Disabling 8-bit/4-bit quantization as "
137
- "bitsandbytes requires CUDA."
138
- )
139
-
140
- model = AutoModelForCausalLM.from_pretrained(model_cfg.name, **kwargs)
141
-
142
- if is_train:
143
- model.config.use_cache = False
144
- model.gradient_checkpointing_enable()
145
- # This forces the model to track gradients for the initial inputs so
146
- # the gradients successfully flow backward to your trainable LoRA adapters
147
- if hasattr(model, "enable_input_require_grads"):
148
- model.enable_input_require_grads()
149
-
150
- # Apply LoRA
151
- if "lora" in model_cfg:
152
- from peft import PeftModel
153
-
154
- if isinstance(model, PeftModel):
155
- logger.info(
156
- "PEFT model detected. Continuing training on existing adapter."
157
- )
158
- # Ensure the existing adapter requires gradients
159
- for name, param in model.named_parameters():
160
- if "lora_" in name:
161
- param.requires_grad = True
162
- else:
163
- lora_cfg = model_cfg.lora
164
- config = LoraConfig(
165
- r=lora_cfg.r,
166
- lora_alpha=lora_cfg.lora_alpha,
167
- lora_dropout=lora_cfg.lora_dropout,
168
- bias=lora_cfg.bias,
169
- task_type=lora_cfg.task_type,
170
- target_modules=list(lora_cfg.target_modules),
171
- )
172
- model = get_peft_model(model, config)
173
- logger.info("Applied native LoRA configuration.")
174
-
175
- return model, tokenizer
176
-
177
-
178
- def run_sft_training(cfg: DictConfig):
179
- """Run Supervised Fine-Tuning."""
180
- logger.info("Initializing SFT Training...")
181
-
182
- model, tokenizer = load_model_and_tokenizer(cfg.model, is_train=True)
183
- from datasets import Features, Sequence, Value
184
- from trl import SFTConfig, SFTTrainer
185
-
186
- sft_features = Features(
187
- {
188
- "instruction": Value("string"),
189
- "input": Value("string"),
190
- "output": Features(
191
- {
192
- "action": Value("string"),
193
- "reasoning": Value("string"),
194
- "facets": Sequence(Value("string")),
195
- "response": Value("string"),
196
- }
197
- ),
198
- }
199
- )
200
-
201
- logger.info(
202
- f"Loading SFT training dataset from {cfg.data.output_sft_train_file}..."
203
- )
204
- dataset_train = load_dataset(
205
- "json", data_files=cfg.data.output_sft_train_file, features=sft_features
206
- )["train"]
207
-
208
- logger.info(
209
- f"Loading SFT validation dataset from {cfg.data.output_sft_val_file}..."
210
- )
211
- dataset_val = load_dataset(
212
- "json", data_files=cfg.data.output_sft_val_file, features=sft_features
213
- )["train"]
214
-
215
- # Format text for training
216
- def format_chat(example):
217
- messages = [
218
- {"role": "system", "content": example["instruction"]},
219
- {"role": "user", "content": example["input"]},
220
- {
221
- "role": "assistant",
222
- "content": (
223
- f"Action: {example['output']['action']}\\n"
224
- f"Reasoning: {example['output']['reasoning']}\\n"
225
- f"Facets: {example['output']['facets']}\\n"
226
- f"Response: {example['output']['response']}"
227
- ),
228
- },
229
- ]
230
- formatted = tokenizer.apply_chat_template(
231
- messages, tokenize=False, add_generation_prompt=False
232
- )
233
- return {"text": formatted}
234
-
235
- formatted_train = dataset_train.map(
236
- format_chat, remove_columns=dataset_train.column_names
237
- )
238
- formatted_val = dataset_val.map(
239
- format_chat, remove_columns=dataset_val.column_names
240
- )
241
-
242
- training_args = SFTConfig(
243
- output_dir=cfg.training.output_dir,
244
- per_device_train_batch_size=cfg.training.per_device_train_batch_size,
245
- gradient_accumulation_steps=cfg.training.gradient_accumulation_steps,
246
- num_train_epochs=cfg.training.num_train_epochs,
247
- max_steps=cfg.training.get("max_steps", -1),
248
- learning_rate=cfg.training.learning_rate,
249
- warmup_ratio=cfg.training.warmup_ratio,
250
- bf16=cfg.training.bf16,
251
- eval_strategy="steps",
252
- eval_steps=cfg.training.logging_steps,
253
- logging_steps=cfg.training.logging_steps,
254
- save_steps=cfg.training.save_steps,
255
- save_total_limit=cfg.training.save_total_limit,
256
- optim=cfg.training.optim,
257
- report_to=cfg.training.report_to,
258
- run_name=os.environ.get("WANDB_RUN_ID", "sft_training"),
259
- dataset_text_field="text",
260
- max_length=cfg.training.max_seq_length,
261
- packing=cfg.training.packing,
262
- remove_unused_columns=cfg.training.get("remove_unused_columns", True),
263
- )
264
-
265
- trainer = SFTTrainer(
266
- model=model,
267
- processing_class=tokenizer,
268
- train_dataset=formatted_train,
269
- eval_dataset=formatted_val,
270
- args=training_args,
271
- )
272
-
273
- logger.info("Starting SFT Training...")
274
- last_checkpoint = None
275
- if cfg.training.get("resume_from_checkpoint", False) and os.path.isdir(
276
- cfg.training.output_dir
277
- ):
278
- last_checkpoint = get_last_checkpoint(cfg.training.output_dir)
279
- if last_checkpoint is not None:
280
- logger.info(f"Resuming SFT training from {last_checkpoint}")
281
-
282
- trainer.train(resume_from_checkpoint=last_checkpoint)
283
-
284
- trainer.save_model(f"{cfg.training.output_dir}/final")
285
- tokenizer.save_pretrained(f"{cfg.training.output_dir}/final")
286
- logger.info("SFT Training complete and model saved.")
287
-
288
-
289
- def run_dpo_training(cfg: DictConfig):
290
- """Run Direct Preference Optimization."""
291
- logger.info("Initializing DPO Training...")
292
-
293
- # Load model and reference model
294
- model, tokenizer = load_model_and_tokenizer(cfg.model, is_train=True)
295
- ref_model, _ = load_model_and_tokenizer(
296
- cfg.model, is_train=False
297
- ) # Ref model without LoRA adapters trainable
298
-
299
- from trl import DPOConfig, DPOTrainer
300
-
301
- logger.info(
302
- f"Loading DPO training dataset from {cfg.data.output_dpo_train_file}..."
303
- )
304
- dataset_train = load_dataset("json", data_files=cfg.data.output_dpo_train_file)[
305
- "train"
306
- ]
307
-
308
- logger.info(
309
- f"Loading DPO validation dataset from {cfg.data.output_dpo_val_file}..."
310
- )
311
- dataset_val = load_dataset("json", data_files=cfg.data.output_dpo_val_file)["train"]
312
-
313
- # Wrap DPO dataset with ChatML to match SFT
314
- def format_dpo(example):
315
- system_prompt = (
316
- "You are a helpful assistant. "
317
- "Given a question, you must decide whether it is ambiguous or not. "
318
- "Output MUST follow this format:\n"
319
- "Action: Clarify|Answer\n"
320
- "Reasoning: <your reasoning>\n"
321
- "Facets: <list of facets if ambiguous, else empty>\n"
322
- "Response: <clarifying question or direct answer>"
323
- )
324
- messages = [
325
- {"role": "system", "content": system_prompt},
326
- {"role": "user", "content": example["prompt"]},
327
- ]
328
-
329
- prompt_str = tokenizer.apply_chat_template(
330
- messages, tokenize=False, add_generation_prompt=True
331
- )
332
-
333
- return {
334
- "prompt": prompt_str,
335
- "chosen": example["chosen"] + tokenizer.eos_token,
336
- "rejected": example["rejected"] + tokenizer.eos_token,
337
- }
338
-
339
- dataset_train = dataset_train.map(format_dpo)
340
- dataset_val = dataset_val.map(format_dpo)
341
-
342
- training_args = DPOConfig(
343
- output_dir=cfg.training.output_dir,
344
- per_device_train_batch_size=cfg.training.per_device_train_batch_size,
345
- gradient_accumulation_steps=cfg.training.gradient_accumulation_steps,
346
- num_train_epochs=cfg.training.num_train_epochs,
347
- max_steps=cfg.training.get("max_steps", -1),
348
- learning_rate=cfg.training.learning_rate,
349
- warmup_ratio=cfg.training.warmup_ratio,
350
- bf16=cfg.training.bf16,
351
- eval_strategy="steps",
352
- eval_steps=cfg.training.logging_steps,
353
- logging_steps=cfg.training.logging_steps,
354
- save_steps=cfg.training.save_steps,
355
- save_total_limit=cfg.training.save_total_limit,
356
- optim=cfg.training.optim,
357
- report_to=cfg.training.report_to,
358
- run_name=os.environ.get("WANDB_RUN_ID", "dpo_training"),
359
- beta=cfg.training.beta,
360
- max_prompt_length=cfg.training.max_prompt_length,
361
- max_length=cfg.training.max_length,
362
- remove_unused_columns=cfg.training.get("remove_unused_columns", False),
363
- )
364
-
365
- trainer = DPOTrainer(
366
- model=model,
367
- ref_model=ref_model,
368
- args=training_args,
369
- train_dataset=dataset_train,
370
- eval_dataset=dataset_val,
371
- processing_class=tokenizer,
372
- )
373
-
374
- logger.info("Starting DPO Training...")
375
- last_checkpoint = None
376
- if cfg.training.get("resume_from_checkpoint", False) and os.path.isdir(
377
- cfg.training.output_dir
378
- ):
379
- last_checkpoint = get_last_checkpoint(cfg.training.output_dir)
380
- if last_checkpoint is not None:
381
- logger.info(f"Resuming DPO training from {last_checkpoint}")
382
-
383
- trainer.train(resume_from_checkpoint=last_checkpoint)
384
-
385
- trainer.save_model(f"{cfg.training.output_dir}/final")
386
- tokenizer.save_pretrained(f"{cfg.training.output_dir}/final")
387
- logger.info("DPO Training complete and model saved.")
388
-
389
-
390
- def run_orpo_training(cfg: DictConfig):
391
- """Run Odds Ratio Preference Optimization."""
392
- logger.info("Initializing ORPO Training...")
393
-
394
- # Load model (NO reference model needed for ORPO)
395
- model, tokenizer = load_model_and_tokenizer(cfg.model, is_train=True)
396
- from trl import ORPOConfig, ORPOTrainer
397
-
398
- logger.info(
399
- f"Loading ORPO training dataset from {cfg.data.output_dpo_train_file}..."
400
- )
401
- dataset_train = load_dataset("json", data_files=cfg.data.output_dpo_train_file)[
402
- "train"
403
- ]
404
-
405
- logger.info(
406
- f"Loading ORPO validation dataset from {cfg.data.output_dpo_val_file}..."
407
- )
408
- dataset_val = load_dataset("json", data_files=cfg.data.output_dpo_val_file)["train"]
409
-
410
- # Wrap DPO dataset with ChatML to match SFT
411
- def format_dpo(example):
412
- system_prompt = (
413
- "You are a helpful assistant. "
414
- "Given a question, you must decide whether it is ambiguous or not. "
415
- "Output MUST follow this format:\n"
416
- "Action: Clarify|Answer\n"
417
- "Reasoning: <your reasoning>\n"
418
- "Facets: <list of facets if ambiguous, else empty>\n"
419
- "Response: <clarifying question or direct answer>"
420
- )
421
- messages = [
422
- {"role": "system", "content": system_prompt},
423
- {"role": "user", "content": example["prompt"]},
424
- ]
425
-
426
- prompt_str = tokenizer.apply_chat_template(
427
- messages, tokenize=False, add_generation_prompt=True
428
- )
429
-
430
- return {
431
- "prompt": prompt_str,
432
- "chosen": example["chosen"] + tokenizer.eos_token,
433
- "rejected": example["rejected"] + tokenizer.eos_token,
434
- }
435
-
436
- dataset_train = dataset_train.map(format_dpo)
437
- dataset_val = dataset_val.map(format_dpo)
438
-
439
- training_args = ORPOConfig(
440
- output_dir=cfg.training.output_dir,
441
- per_device_train_batch_size=cfg.training.per_device_train_batch_size,
442
- gradient_accumulation_steps=cfg.training.gradient_accumulation_steps,
443
- num_train_epochs=cfg.training.num_train_epochs,
444
- max_steps=cfg.training.get("max_steps", -1),
445
- learning_rate=cfg.training.learning_rate,
446
- warmup_ratio=cfg.training.warmup_ratio,
447
- bf16=cfg.training.bf16,
448
- eval_strategy="steps",
449
- eval_steps=cfg.training.logging_steps,
450
- logging_steps=cfg.training.logging_steps,
451
- save_steps=cfg.training.save_steps,
452
- save_total_limit=cfg.training.save_total_limit,
453
- optim=cfg.training.optim,
454
- report_to=cfg.training.report_to,
455
- run_name=os.environ.get("WANDB_RUN_ID", "orpo_training"),
456
- beta=cfg.training.beta,
457
- max_prompt_length=cfg.training.max_prompt_length,
458
- max_length=cfg.training.max_length,
459
- remove_unused_columns=cfg.training.get("remove_unused_columns", False),
460
- )
461
-
462
- trainer = ORPOTrainer(
463
- model=model,
464
- args=training_args,
465
- train_dataset=dataset_train,
466
- eval_dataset=dataset_val,
467
- processing_class=tokenizer,
468
- )
469
-
470
- logger.info("Starting ORPO Training...")
471
- last_checkpoint = None
472
- if cfg.training.get("resume_from_checkpoint", False) and os.path.isdir(
473
- cfg.training.output_dir
474
- ):
475
- last_checkpoint = get_last_checkpoint(cfg.training.output_dir)
476
- if last_checkpoint is not None:
477
- logger.info(f"Resuming ORPO training from {last_checkpoint}")
478
-
479
- trainer.train(resume_from_checkpoint=last_checkpoint)
480
-
481
- trainer.save_model(f"{cfg.training.output_dir}/final")
482
- tokenizer.save_pretrained(f"{cfg.training.output_dir}/final")
483
- logger.info("ORPO Training complete and model saved.")
484
-
485
-
486
- def format_reward_func(prompts, completions, reward_weights=None, **kwargs):
487
- """Reward function that checks for the exact format constraints."""
488
- if reward_weights is None:
489
- reward_weights = {}
490
- format_reward = reward_weights.get("format_reward", 1.0)
491
- format_penalty = reward_weights.get("format_penalty", -2.0)
492
-
493
- rewards = []
494
- for completion in completions:
495
- # A simple check: do we have all the sections in order?
496
- text = completion[0]["content"] if isinstance(completion, list) else completion
497
- has_action = "Action:" in text
498
- has_reasoning = "Reasoning:" in text
499
- has_facets = "Facets:" in text
500
- has_response = "Response:" in text
501
-
502
- if has_action and has_reasoning and has_facets and has_response:
503
- rewards.append(format_reward)
504
- else:
505
- rewards.append(format_penalty)
506
- return rewards
507
-
508
-
509
- def action_reward_func(prompts, completions, reward_weights=None, **kwargs):
510
- """Reward function that checks if the predicted action matches the target."""
511
- if reward_weights is None:
512
- reward_weights = {}
513
- action_reward = reward_weights.get("action_reward", 1.0)
514
- action_penalty = reward_weights.get("action_penalty", -1.0)
515
-
516
- rewards = []
517
- target_actions = kwargs.get("target_action", [])
518
-
519
- for i, completion in enumerate(completions):
520
- text = completion[0]["content"] if isinstance(completion, list) else completion
521
- target = target_actions[i]
522
-
523
- action_match = re.search(r"Action:\s*(Clarify|Answer)", text)
524
- if action_match:
525
- pred_action = action_match.group(1)
526
- if pred_action == target:
527
- rewards.append(action_reward)
528
- else:
529
- rewards.append(action_penalty)
530
- else:
531
- rewards.append(action_penalty)
532
- return rewards
533
-
534
-
535
- def facet_logic_reward_func(prompts, completions, reward_weights=None, **kwargs):
536
- """Reward function that checks facet presence/absence based on action."""
537
- if reward_weights is None:
538
- reward_weights = {}
539
- facet_reward = reward_weights.get("facet_logic_reward", 0.5)
540
- facet_penalty = reward_weights.get("facet_logic_penalty", -0.5)
541
-
542
- rewards = []
543
- for completion in completions:
544
- text = completion[0]["content"] if isinstance(completion, list) else completion
545
- action_match = re.search(r"Action:\s*(Clarify|Answer)", text)
546
- facets_match = re.search(r"Facets:\s*(\[.*?\])", text, re.DOTALL)
547
-
548
- if not action_match or not facets_match:
549
- rewards.append(facet_penalty)
550
- continue
551
-
552
- pred_action = action_match.group(1)
553
- facets_str = facets_match.group(1)
554
-
555
- try:
556
- facets = ast.literal_eval(facets_str)
557
- if not isinstance(facets, list):
558
- facets = []
559
- except Exception:
560
- facets = []
561
-
562
- if pred_action == "Clarify":
563
- # Clarify MUST have non-empty facets
564
- if len(facets) > 0:
565
- rewards.append(facet_reward)
566
- else:
567
- rewards.append(facet_penalty)
568
- else:
569
- # Answer MUST have empty facets
570
- if len(facets) == 0:
571
- rewards.append(facet_reward)
572
- else:
573
- rewards.append(facet_penalty)
574
- return rewards
575
-
576
-
577
- def accuracy_reward_func(prompts, completions, reward_weights=None, **kwargs):
578
- """Reward function that checks for factual accuracy (word overlap)
579
- for direct answers."""
580
- if reward_weights is None:
581
- reward_weights = {}
582
- acc_scale = reward_weights.get("accuracy_scale", 1.5)
583
- acc_shift = reward_weights.get("accuracy_shift", -0.5)
584
- acc_miss_penalty = reward_weights.get("accuracy_miss_penalty", -1.0)
585
- acc_format_penalty = reward_weights.get("accuracy_format_penalty", -0.5)
586
-
587
- rewards = []
588
- target_responses = kwargs.get("target_response", [])
589
- target_actions = kwargs.get("target_action", [])
590
-
591
- for i, completion in enumerate(completions):
592
- text = completion[0]["content"] if isinstance(completion, list) else completion
593
- target_resp = target_responses[i]
594
- target_act = target_actions[i]
595
-
596
- # Only heavily shape accuracy for direct answers to prevent hallucination.
597
- # Clarification questions are too linguistically diverse to strictly grade
598
- # with word overlap.
599
- if target_act != "Answer":
600
- rewards.append(0.0)
601
- continue
602
-
603
- response_match = re.search(r"Response:\s*(.*)", text, re.DOTALL)
604
- if not response_match or not target_resp:
605
- rewards.append(acc_format_penalty)
606
- continue
607
-
608
- pred_resp = response_match.group(1).strip()
609
-
610
- # Token overlap F1
611
- pred_words = set(re.findall(r"\b\w+\b", pred_resp.lower()))
612
- target_words = set(re.findall(r"\b\w+\b", target_resp.lower()))
613
-
614
- if not target_words:
615
- rewards.append(0.0)
616
- continue
617
-
618
- intersection = pred_words.intersection(target_words)
619
-
620
- if not intersection:
621
- rewards.append(acc_miss_penalty) # Totally missed the facts
622
- continue
623
-
624
- recall = len(intersection) / len(target_words)
625
- precision = len(intersection) / len(pred_words)
626
-
627
- f1 = 2 * (precision * recall) / (precision + recall)
628
-
629
- # Map F1 to reward: low overlap is penalized,
630
- # high overlap is moderately rewarded
631
- reward = (f1 * acc_scale) + acc_shift
632
- rewards.append(reward)
633
-
634
- return rewards
635
-
636
-
637
- def run_grpo_training(cfg: DictConfig):
638
- """Run Group Relative Policy Optimization."""
639
- logger.info("Initializing GRPO Training...")
640
-
641
- model, tokenizer = load_model_and_tokenizer(cfg.model, is_train=True)
642
- from trl import GRPOConfig, GRPOTrainer
643
-
644
- logger.info(
645
- f"Loading GRPO training dataset from {cfg.data.output_dpo_train_file}..."
646
- )
647
- dataset_train = load_dataset("json", data_files=cfg.data.output_dpo_train_file)[
648
- "train"
649
- ]
650
- logger.info(
651
- f"Loading GRPO validation dataset from {cfg.data.output_dpo_val_file}..."
652
- )
653
- dataset_val = load_dataset("json", data_files=cfg.data.output_dpo_val_file)["train"]
654
-
655
- def format_grpo(example):
656
- system_prompt = (
657
- "You are a helpful assistant. "
658
- "Given a question, you must decide whether it is ambiguous or not. "
659
- "Output MUST follow this format:\n"
660
- "Action: Clarify|Answer\n"
661
- "Reasoning: <your reasoning>\n"
662
- "Facets: <list of facets if ambiguous, else empty>\n"
663
- "Response: <clarifying question or direct answer>"
664
- )
665
- messages = [
666
- {"role": "system", "content": system_prompt},
667
- {"role": "user", "content": example["prompt"]},
668
- ]
669
-
670
- prompt_str = tokenizer.apply_chat_template(
671
- messages, tokenize=False, add_generation_prompt=True
672
- )
673
-
674
- # Extract the target action from the 'chosen' string in the DPO dataset
675
- target_action = "Answer"
676
- if "Action: Clarify" in example["chosen"]:
677
- target_action = "Clarify"
678
-
679
- target_response = ""
680
- response_match = re.search(r"Response:\s*(.*)", example["chosen"], re.DOTALL)
681
- if response_match:
682
- target_response = response_match.group(1).strip()
683
-
684
- return {
685
- "prompt": prompt_str,
686
- "target_action": target_action,
687
- "target_response": target_response,
688
- }
689
-
690
- dataset_train = dataset_train.map(
691
- format_grpo, remove_columns=dataset_train.column_names
692
- )
693
- dataset_val = dataset_val.map(format_grpo, remove_columns=dataset_val.column_names)
694
-
695
- training_args = GRPOConfig(
696
- output_dir=cfg.training.output_dir,
697
- per_device_train_batch_size=cfg.training.per_device_train_batch_size,
698
- gradient_accumulation_steps=cfg.training.gradient_accumulation_steps,
699
- num_train_epochs=cfg.training.num_train_epochs,
700
- max_steps=cfg.training.get("max_steps", -1),
701
- learning_rate=cfg.training.learning_rate,
702
- warmup_ratio=cfg.training.warmup_ratio,
703
- bf16=cfg.training.bf16,
704
- logging_steps=cfg.training.logging_steps,
705
- save_steps=cfg.training.save_steps,
706
- save_total_limit=cfg.training.save_total_limit,
707
- optim=cfg.training.optim,
708
- report_to=cfg.training.report_to,
709
- run_name=os.environ.get("WANDB_RUN_ID", "grpo_training"),
710
- beta=cfg.training.beta,
711
- max_prompt_length=cfg.training.max_prompt_length,
712
- max_completion_length=cfg.training.max_completion_length,
713
- num_generations=cfg.training.num_generations,
714
- loss_type=cfg.training.get("loss_type", "grpo"),
715
- )
716
-
717
- # Bugfix for UnslothGRPOTrainer CPU/CUDA device mismatch during generation
718
- original_generate = model.generate
719
-
720
- def patched_generate(*args, **kwargs):
721
- if len(args) > 0 and isinstance(args[0], torch.Tensor):
722
- args = list(args)
723
- args[0] = args[0].to(model.device)
724
- args = tuple(args)
725
- if "input_ids" in kwargs and isinstance(kwargs["input_ids"], torch.Tensor):
726
- kwargs["input_ids"] = kwargs["input_ids"].to(model.device)
727
- if "attention_mask" in kwargs and isinstance(
728
- kwargs["attention_mask"], torch.Tensor
729
- ):
730
- kwargs["attention_mask"] = kwargs["attention_mask"].to(model.device)
731
- return original_generate(*args, **kwargs)
732
-
733
- model.generate = patched_generate
734
-
735
- def make_reward_functions(reward_weights: dict[str, Any]) -> list[Any]:
736
- """Create GRPO reward functions with bound reward weights.
737
-
738
- The returned objects are regular Python functions rather than
739
- functools.partial instances because Unsloth expects reward functions
740
- to expose a __name__ attribute.
741
- """
742
-
743
- def format_reward(prompts, completions, **kwargs):
744
- return format_reward_func(
745
- prompts,
746
- completions,
747
- reward_weights=reward_weights,
748
- **kwargs,
749
- )
750
-
751
- def action_reward(prompts, completions, **kwargs):
752
- return action_reward_func(
753
- prompts,
754
- completions,
755
- reward_weights=reward_weights,
756
- **kwargs,
757
- )
758
-
759
- def facet_logic_reward(prompts, completions, **kwargs):
760
- return facet_logic_reward_func(
761
- prompts,
762
- completions,
763
- reward_weights=reward_weights,
764
- **kwargs,
765
- )
766
-
767
- def accuracy_reward(prompts, completions, **kwargs):
768
- return accuracy_reward_func(
769
- prompts,
770
- completions,
771
- reward_weights=reward_weights,
772
- **kwargs,
773
- )
774
-
775
- return [
776
- format_reward,
777
- action_reward,
778
- facet_logic_reward,
779
- accuracy_reward,
780
- ]
781
-
782
- reward_weights = cfg.training.get("reward_weights", {})
783
-
784
- trainer = GRPOTrainer(
785
- model=model,
786
- reward_funcs=make_reward_functions(reward_weights),
787
- args=training_args,
788
- train_dataset=dataset_train,
789
- eval_dataset=dataset_val,
790
- processing_class=tokenizer,
791
- )
792
-
793
- logger.info("Starting GRPO Training...")
794
- last_checkpoint = None
795
- if cfg.training.get("resume_from_checkpoint", False) and os.path.isdir(
796
- cfg.training.output_dir
797
- ):
798
- last_checkpoint = get_last_checkpoint(cfg.training.output_dir)
799
- if last_checkpoint is not None:
800
- logger.info(f"Resuming GRPO training from {last_checkpoint}")
801
-
802
- trainer.train(resume_from_checkpoint=last_checkpoint)
803
-
804
- trainer.save_model(f"{cfg.training.output_dir}/final")
805
- tokenizer.save_pretrained(f"{cfg.training.output_dir}/final")
806
- logger.info("GRPO Training complete and model saved.")