| """ |
| DDO SDAR Training Script - Full Parameter Fine-tuning |
| """ |
| import os |
| import sys |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| os.environ["TOKENIZERS_PARALLELISM"] = "true" |
|
|
| import json |
| import logging |
| import math |
| import shutil |
| import time |
| import wandb |
| import torch |
| import numpy as np |
| import torch.nn.functional as F |
|
|
| from pathlib import Path |
| from omegaconf import OmegaConf |
| from torch.utils.data import Dataset, DataLoader |
|
|
| from torch.optim import AdamW |
| from transformers import AutoTokenizer, AutoModelForCausalLM |
|
|
| from accelerate import Accelerator |
| from accelerate.logging import get_logger |
| from accelerate.utils import set_seed |
|
|
| from train.prompting_utils import UniversalPrompting |
| from models.lr_schedulers import get_scheduler |
| from models.logging import set_verbosity_info, set_verbosity_error |
|
|
| from train.prepare import collect_training_data, make_basic_block_attention, process_pad |
| from train.utils import get_config, flatten_omega_conf |
|
|
| try: |
| import apex |
| is_apex_available = True |
| except ImportError: |
| is_apex_available = False |
|
|
| logger = get_logger(__name__, log_level="INFO") |
|
|
|
|
| def get_models_full_finetune(config): |
| """ |
| Full parameter fine-tuning version. |
| Returns: (model, ref_model, tokenizer) |
| - model: the trainable model |
| - ref_model: frozen reference model (separate copy) |
| """ |
| pretrained_model = config.model.pretrained_model |
| |
| |
| model = AutoModelForCausalLM.from_pretrained( |
| pretrained_model, |
| trust_remote_code=True, |
| torch_dtype=torch.bfloat16 |
| ) |
| tokenizer = AutoTokenizer.from_pretrained(pretrained_model, trust_remote_code=True) |
| |
| |
| ref_model = AutoModelForCausalLM.from_pretrained( |
| pretrained_model, |
| trust_remote_code=True, |
| torch_dtype=torch.bfloat16 |
| ) |
| |
| for param in ref_model.parameters(): |
| param.requires_grad = False |
| ref_model.eval() |
|
|
| n_trainable = sum(p.numel() for p in model.parameters() if p.requires_grad) |
| print(f"Number of trainable parameters: {n_trainable:,}") |
|
|
| return model, ref_model, tokenizer |
|
|
|
|
| def copy_model_weights(src_model, dst_model): |
| """ |
| Copy all weights from src_model to dst_model for multi-round refinement. |
| This is used in self-play to update the reference model after each round. |
| """ |
| with torch.no_grad(): |
| src_state = src_model.state_dict() |
| dst_model.load_state_dict(src_state) |
| print("Copied model weights from training model to reference model") |
|
|
|
|
| class TrainDataset(Dataset): |
| def __init__(self, extended_input_ids, p_mask, tok_idx_ext, labels, step_map_list): |
| self.extended_input_ids = extended_input_ids |
| self.p_mask = p_mask |
| self.tok_idx_ext = tok_idx_ext |
| self.labels = labels |
| self.step_map_list = step_map_list |
| def __len__(self): |
| return len(self.extended_input_ids) |
|
|
| def __getitem__(self, idx): |
| return ( |
| idx, |
| self.extended_input_ids[idx], |
| self.p_mask[idx], |
| self.tok_idx_ext[idx], |
| self.labels[idx], |
| self.step_map_list[idx] |
| ) |
|
|
| def simple_collate(batch): |
| idx, extended_input_ids, p_mask, tok_idx_ext, labels, step_map_list = zip(*batch) |
| |
| step_map_tensors = [] |
| for sm in step_map_list: |
| if isinstance(sm, (list, tuple)): |
| step_map_tensors.append(torch.tensor(sm, dtype=torch.long)) |
| elif isinstance(sm, torch.Tensor): |
| step_map_tensors.append(sm) |
| else: |
| step_map_tensors.append(torch.tensor(sm, dtype=torch.long)) |
|
|
| B = len(labels) |
| if B > 0: |
| max_len = max(sm.shape[0] if isinstance(sm, torch.Tensor) else len(sm) for sm in step_map_tensors) |
| |
| step_map_padded = [] |
| for sm in step_map_tensors: |
| if sm.shape[0] < max_len: |
| padding = torch.full((max_len - sm.shape[0],), -1, dtype=sm.dtype) |
| sm = torch.cat([sm, padding], dim=0) |
| step_map_padded.append(sm) |
| |
| step_map_stacked = torch.stack(step_map_padded) |
| else: |
| step_map_stacked = torch.empty((0, 0), dtype=torch.long) |
| |
| return { |
| "ids": torch.tensor(idx), |
| "extended_input_ids": torch.stack(extended_input_ids), |
| "p_mask": torch.stack(p_mask), |
| "tok_idx_ext": torch.stack(tok_idx_ext), |
| "labels": torch.stack(labels), |
| "step_map_list": step_map_stacked |
| } |
|
|
|
|
| def add_gumbel_noise(logits, temperature): |
| ''' |
| The Gumbel max is a method for sampling categorical distributions. |
| According to arXiv:2409.02908, for MDM, low-precision Gumbel Max improves perplexity score but reduces generation quality. |
| Thus, we use float64. |
| ''' |
| if temperature == 0: |
| return logits |
| |
| logits = logits.to(torch.float64) |
| noise = torch.rand_like(logits, dtype=torch.float64) |
| gumbel_noise = (- torch.log(noise)) ** temperature |
| return logits.exp() / gumbel_noise |
|
|
|
|
| def compute_logp(model, ref_model, extended_input_ids, p_mask, tok_idx_ext, labels, start_pos, basic_block_attention, pad_id, reference_logits=False): |
| B, L = p_mask.shape |
| L0 = start_pos |
| L1 = L - L0 |
| device = extended_input_ids.device |
| attention_mask = basic_block_attention.clone() |
| attention_mask = attention_mask.repeat_interleave(B, dim=0).to(device) |
| attention_mask = process_pad(attention_mask, extended_input_ids, L0, L1, start_pos, pad_id) |
| |
| if reference_logits: |
| |
| logits = ref_model(input_ids=extended_input_ids, attention_mask=attention_mask, position_ids=tok_idx_ext).logits |
| else: |
| |
| logits = model(input_ids=extended_input_ids, attention_mask=attention_mask, position_ids=tok_idx_ext).logits |
| |
| logits = torch.cat([logits[:, :L0, :], logits[:, L0 + L1:, :]], dim=1) |
| log_probs = F.log_softmax(logits, dim=-1) |
| logp_tok = log_probs.gather(dim=-1, index=labels.unsqueeze(-1)).squeeze(-1) |
| logp = (logp_tok * p_mask).sum(dim=1) / p_mask.sum(dim=1) |
| return logp |
|
|
|
|
| def compute_logp_fake(model, ref_model, extended_input_ids, p_mask, tok_idx_ext, labels, start_pos, basic_block_attention, pad_id, temperature, reference_logits=False, x_theta_fake=None): |
| B, L = p_mask.shape |
| L0 = start_pos |
| L1 = L - L0 |
|
|
| device = extended_input_ids.device |
| attention_mask = basic_block_attention.clone() |
| attention_mask = attention_mask.repeat_interleave(B, dim=0).to(device) |
| attention_mask = process_pad(attention_mask, extended_input_ids, L0, L1, start_pos, pad_id) |
| |
| if reference_logits: |
| |
| logits = ref_model(input_ids=extended_input_ids, attention_mask=attention_mask, position_ids=tok_idx_ext).logits |
| else: |
| |
| logits = model(input_ids=extended_input_ids, attention_mask=attention_mask, position_ids=tok_idx_ext).logits |
|
|
| logits = torch.cat([logits[:, :L0, :], logits[:, L0 + L1:, :]], dim=1) |
| if x_theta_fake is None: |
| if temperature == 0.0: |
| x_theta = torch.argmax(logits, dim=-1) |
| else: |
| logits_with_noise = add_gumbel_noise(logits, temperature=temperature) |
| x_theta = torch.argmax(logits_with_noise, dim=-1) |
| else: |
| x_theta = x_theta_fake |
| |
| log_probs = F.log_softmax(logits, dim=-1) |
| logp_tok = log_probs.gather(dim=-1, index=x_theta.unsqueeze(-1)).squeeze(-1) |
| logp = (logp_tok * p_mask).sum(dim=1) / p_mask.sum(dim=1) |
| return logp, logits |
|
|
|
|
| def compute_loss(config, target_logp_real, target_logp_fake, reference_logp_real, reference_logp_fake): |
| alpha = config.training.alpha |
| beta = config.training.beta |
|
|
| loss_real = -F.logsigmoid(beta * (target_logp_real - reference_logp_real)) |
| sigmoid_val = torch.sigmoid(beta * (target_logp_fake - reference_logp_fake)) |
| loss_fake = -alpha * torch.log(1 - sigmoid_val) |
|
|
| loss = loss_real.mean() + loss_fake.mean() |
|
|
| metrics = { |
| 'loss': loss.item(), |
| 'loss_real': loss_real.mean().item(), |
| 'loss_fake': loss_fake.mean().item(), |
| 'logp_target_real': target_logp_real.mean().item(), |
| 'logp_ref_real': reference_logp_real.mean().item(), |
| 'logp_target_fake': target_logp_fake.mean().item(), |
| 'logp_ref_fake': reference_logp_fake.mean().item(), |
| } |
| return loss, metrics |
|
|
|
|
| def save_checkpoint(model, tokenizer, config, accelerator, name, project_timestamp_dir): |
| |
| output_dir = project_timestamp_dir |
| output_dir.mkdir(parents=True, exist_ok=True) |
|
|
| checkpoints_total_limit = config.experiment.get("checkpoints_total_limit", None) |
|
|
| if accelerator.is_main_process and checkpoints_total_limit is not None: |
| ckpts = sorted( |
| [d for d in output_dir.iterdir() if d.name.startswith("checkpoint")], |
| key=lambda p: int(p.name.split("-")[1]), |
| ) |
| if len(ckpts) >= checkpoints_total_limit: |
| to_remove = ckpts[: len(ckpts) - checkpoints_total_limit + 1] |
| logger.info(f"removing checkpoints: {', '.join(p.name for p in to_remove)}") |
| for p in to_remove: |
| shutil.rmtree(p, ignore_errors=True) |
|
|
| save_base = output_dir / "ckpt" |
| save_base.mkdir(exist_ok=True) |
| |
| if accelerator.is_main_process: |
| unwrapped_model = accelerator.unwrap_model(model) |
| unwrapped_model.save_pretrained( |
| save_base / name, |
| safe_serialization=True, |
| ) |
| logger.info(f"Saved model to {save_base / name}") |
|
|
| |
| tokenizer.save_pretrained(str(save_base / name)) |
|
|
| metadata = { |
| "save_time": time.strftime("%Y-%m-%d %H:%M:%S"), |
| } |
| with (save_base / name / "metadata.json").open("w") as f: |
| json.dump(metadata, f, indent=2) |
|
|
| |
| OmegaConf.save(config, save_base / name / "config.yaml") |
|
|
| logger.info(f"Saved model + tokenizer to {save_base / name}") |
|
|
|
|
| def reconstruct_xt_from_step_map(x0, step_map, block_size, k_steps, mask_id, pad_id, start_pos): |
| """ |
| Reconstruct xt from x0 using step_map decode order. |
| |
| Args: |
| x0: [B, L] clean tokens (full sequence including prefix) |
| step_map: [B, L1] decode order for response tokens (lower = decoded earlier) |
| block_size: size of each block |
| k_steps: number of tokens per large step |
| mask_id: mask token id |
| pad_id: pad token id |
| start_pos: start position of response (L0) |
| |
| Returns: |
| List of xt tensors, one for each step level: [xt_step0, xt_step1, ..., xt_stepN] |
| Each xt is [B, L] with masked tokens according to decode order |
| """ |
| B, L = x0.shape |
| L0 = start_pos |
| L1 = L - L0 |
| device = x0.device |
| |
| |
| x0_resp = x0[:, start_pos:] |
| |
| |
| if step_map.dim() == 1: |
| |
| step_map = step_map.unsqueeze(0) |
| if step_map.shape[0] != B: |
| step_map = step_map.expand(B, -1) |
| |
| step_map_L1 = step_map.shape[1] |
| if step_map_L1 != L1: |
| |
| if step_map_L1 > L1: |
| |
| step_map = step_map[:, :L1] |
| else: |
| |
| pad_len = L1 - step_map_L1 |
| step_map = torch.cat([ |
| step_map, |
| torch.full((B, pad_len), -1, dtype=step_map.dtype, device=device) |
| ], dim=1) |
| |
| |
| NB = (L1 + block_size - 1) // block_size |
| steps_per_block = (block_size + k_steps - 1) // k_steps |
| total_steps = NB * steps_per_block |
| |
| |
| pad_len = NB * block_size - L1 |
| if pad_len > 0: |
| step_map_pad = torch.cat([ |
| step_map, |
| torch.full((B, pad_len), -1, dtype=step_map.dtype, device=device) |
| ], dim=1) |
| x0_resp_pad = torch.cat([ |
| x0_resp, |
| torch.full((B, pad_len), pad_id, dtype=x0_resp.dtype, device=device) |
| ], dim=1) |
| else: |
| step_map_pad = step_map |
| x0_resp_pad = x0_resp |
| |
| |
| step_map_blocks = step_map_pad.view(B, NB, block_size) |
| x0_resp_blocks = x0_resp_pad.view(B, NB, block_size) |
| |
| |
| xt_list = [] |
| |
| |
| for step_level in range(steps_per_block): |
| |
| |
| |
| mask_blocks = torch.ones(B, NB, block_size, dtype=torch.bool, device=device) |
| |
| |
| for bi in range(NB): |
| block_step_map = step_map_blocks[:, bi, :] |
| |
| |
| for b in range(B): |
| block_sm = block_step_map[b] |
| |
| |
| valid_mask = block_sm >= 0 |
| valid_count = valid_mask.sum().item() |
| |
| if valid_count == 0: |
| |
| continue |
| |
| |
| num_to_decode = min((step_level + 1) * k_steps, valid_count) |
| |
| if num_to_decode > 0: |
| |
| valid_step_map = block_sm[valid_mask] |
| valid_indices = torch.where(valid_mask)[0] |
| |
| |
| sorted_indices = torch.argsort(valid_step_map) |
| |
| decode_indices = valid_indices[sorted_indices[:num_to_decode]] |
| |
| |
| mask_blocks[b, bi, decode_indices] = False |
| |
| |
| already_decoded = block_sm == -1 |
| mask_blocks[b, bi, :] = mask_blocks[b, bi, :] & (~already_decoded) |
| |
| |
| xt_resp_blocks = x0_resp_blocks.clone() |
| xt_resp_blocks[mask_blocks] = mask_id |
| |
| |
| pad_mask = x0_resp_blocks == pad_id |
| xt_resp_blocks[pad_mask] = pad_id |
| |
| |
| xt_resp_pad = xt_resp_blocks.view(B, -1) |
| |
| |
| xt_resp = xt_resp_pad[:, :L1] |
| |
| |
| xt = torch.cat([x0[:, :start_pos], xt_resp], dim=1) |
| |
| xt_list.append(xt) |
| |
| return xt_list |
|
|
|
|
| def generate_xt_from_step_map(x0, step_map, block_size, k_steps, mask_id, pad_id, start_pos): |
| """ |
| Generate true xt from ground truth x0 using step_map decode order. |
| Same logic as reconstruct_xt_from_step_map but uses ground truth x0. |
| |
| Args: |
| x0: [B, L] ground truth clean tokens |
| step_map: [B, L1] decode order for response tokens |
| block_size: size of each block |
| k_steps: number of tokens per large step |
| mask_id: mask token id |
| pad_id: pad token id |
| start_pos: start position of response (L0) |
| |
| Returns: |
| List of xt tensors, one for each step level: [xt_step0, xt_step1, ..., xt_stepN] |
| """ |
| |
| return reconstruct_xt_from_step_map(x0, step_map, block_size, k_steps, mask_id, pad_id, start_pos) |
|
|
|
|
| def compute_path_loss_from_step_map(logits_x0, x0_true, step_map, block_size, k_steps, mask_id, pad_id, start_pos): |
| """ |
| Compute path loss between predicted xt and true xt using step_map decode order. |
| |
| Args: |
| logits_x0: [B, L, V] logits from target model |
| x0_true: [B, L] ground truth clean tokens |
| step_map: [B, L1] decode order for response tokens |
| block_size: size of each block |
| k_steps: number of tokens per large step |
| mask_id: mask token id |
| pad_id: pad token id |
| start_pos: start position of response (L0) |
| |
| Returns: |
| Scalar loss value (averaged across all step levels) |
| """ |
| device = logits_x0.device |
| B, L, V = logits_x0.shape |
| L0 = start_pos |
| L1 = L - L0 |
| |
| |
| x0_pred = torch.argmax(logits_x0, dim=-1) |
| |
| |
| xt_pred_list = reconstruct_xt_from_step_map(x0_pred, step_map, block_size, k_steps, mask_id, pad_id, start_pos) |
| |
| |
| xt_true_list = generate_xt_from_step_map(x0_true, step_map, block_size, k_steps, mask_id, pad_id, start_pos) |
| |
| |
| step_losses = [] |
| |
| for xt_pred, xt_true in zip(xt_pred_list, xt_true_list): |
| |
| |
| xt_true_resp = xt_true[:, start_pos:] |
| x0_true_resp = x0_true[:, start_pos:] |
| |
| |
| |
| |
| non_masked_positions = (xt_true_resp != mask_id) & (x0_true_resp != pad_id) |
| |
| if not non_masked_positions.any(): |
| |
| continue |
| |
| |
| logits_resp = logits_x0[:, start_pos:, :] |
| |
| |
| |
| non_masked_positions_flat = non_masked_positions.view(-1) |
| logits_flat = logits_resp.view(-1, V) |
| targets_flat = xt_true_resp.view(-1) |
| |
| |
| logits_non_masked = logits_flat[non_masked_positions_flat] |
| targets_non_masked = targets_flat[non_masked_positions_flat] |
| |
| if len(targets_non_masked) > 0: |
| |
| loss_step = F.cross_entropy(logits_non_masked, targets_non_masked, reduction='mean') |
| step_losses.append(loss_step) |
| |
| |
| if len(step_losses) > 0: |
| total_loss = sum(step_losses) / len(step_losses) |
| else: |
| |
| total_loss = torch.tensor(0.0, device=device, requires_grad=True) |
| |
| return total_loss |
|
|
|
|
| def main(): |
| |
| |
| |
| config = get_config() |
|
|
| project_name = config.experiment.project |
| pretrained_model = config.model.pretrained_model |
|
|
| |
| timestamp = time.strftime("%Y%m%d_%H%M") |
| project_timestamp_dir = Path("experiments") / pretrained_model.split("/")[-1] / config.dataset.data_type / project_name / timestamp |
| project_timestamp_dir.mkdir(parents=True, exist_ok=True) |
|
|
| |
| if config.training.enable_tf32: |
| torch.backends.cuda.matmul.allow_tf32 = True |
| torch.backends.cudnn.benchmark = True |
| torch.backends.cudnn.deterministic = False |
|
|
| config.experiment.logging_dir = str(project_timestamp_dir / "logs") |
| accelerator = Accelerator( |
| gradient_accumulation_steps=config.training.gradient_accumulation_steps, |
| mixed_precision=config.training.mixed_precision, |
| log_with="wandb", |
| project_dir=config.experiment.logging_dir, |
| split_batches=True, |
| ) |
| |
| logging.basicConfig( |
| format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", |
| datefmt="%m/%d/%Y %H:%M:%S", |
| level=logging.INFO, |
| ) |
| logger.info(accelerator.state, main_process_only=False) |
| if accelerator.is_local_main_process: |
| set_verbosity_info() |
| else: |
| set_verbosity_error() |
|
|
| if accelerator.is_main_process: |
| resume_wandb_run = config.wandb.resume |
| run_id = config.wandb.get("run_id", None) |
| if run_id is None: |
| resume_wandb_run = False |
| run_id = wandb.util.generate_id() |
| config.wandb.run_id = run_id |
|
|
| wandb_init_kwargs = dict( |
| name=config.experiment.project, |
| id=run_id, |
| resume=resume_wandb_run, |
| entity=config.wandb.get("entity", None), |
| config_exclude_keys=[], |
| ) |
| wandb_config = {k: v for k, v in flatten_omega_conf(config, resolve=True)} |
| wandb_config.pop("experiment.resume_from_checkpoint", None) |
| accelerator.init_trackers( |
| config.experiment.project, |
| config=wandb_config, |
| init_kwargs={"wandb": wandb_init_kwargs}, |
| ) |
|
|
| if accelerator.is_main_process: |
| os.makedirs(project_timestamp_dir, exist_ok=True) |
| config_path = project_timestamp_dir / "config.yaml" |
| logging.info(f"Saving config to {config_path}") |
| OmegaConf.save(config, config_path) |
|
|
| |
| if config.training.seed is not None: |
| set_seed(config.training.seed) |
|
|
|
|
| |
| |
| |
| logger.info("Loading models and optimizer (Full Parameter Fine-tuning)") |
| |
| |
| multi_round = getattr(config.training, "multi_round", False) |
| round_interval = getattr(config.training, "round_interval", 100) |
| |
| if multi_round: |
| logger.info(f"Multi-round refinement enabled with round_interval={round_interval} global steps") |
| |
| |
| model, ref_model, tokenizer = get_models_full_finetune(config) |
|
|
| uni_prompting = UniversalPrompting(tokenizer, max_prompt_len=config.training.max_prompt_len, |
| max_gen_length=config.training.max_gen_length, |
| ignore_id=-100) |
|
|
| |
| |
| if hasattr(model, "config"): |
| model.config.fuse_cross_entropy = False |
| if hasattr(ref_model, "config"): |
| ref_model.config.fuse_cross_entropy = False |
|
|
| if config.training.gradient_checkpointing_enable: |
| model.gradient_checkpointing_enable() |
| if hasattr(model, "config"): |
| model.config.use_cache = False |
| else: |
| model = model.to(accelerator.device) |
| |
| |
| ref_model = ref_model.to(accelerator.device) |
|
|
| mask_id = tokenizer.mask_token_id |
| pad_id = tokenizer.pad_token_id |
| vocab_size = len(tokenizer) |
|
|
|
|
| |
| |
| |
| optimizer_config = config.optimizer.params |
|
|
| |
| params = [ |
| { |
| "params": [p for p in model.parameters() if p.requires_grad], |
| "weight_decay": optimizer_config.weight_decay, |
| }, |
| ] |
|
|
| optimizer_type = config.optimizer.name |
| if optimizer_type == "adamw": |
| optimizer = AdamW( |
| params, |
| lr=optimizer_config.learning_rate, |
| betas=(optimizer_config.beta1, optimizer_config.beta2), |
| weight_decay=optimizer_config.weight_decay if optimizer_config.weight_decay is not None else 0.0, |
| eps=optimizer_config.epsilon, |
| ) |
| else: |
| raise ValueError(f"Optimizer {optimizer_type} not supported") |
|
|
| |
| if accelerator.is_main_process: |
| logger.info("=" * 80) |
| logger.info("PARAMETER MONITORING (Full Fine-tuning)") |
| params_list = [(n, p) for n, p in model.named_parameters() if p.requires_grad] |
| total_params = sum(p.numel() for _, p in params_list) |
| logger.info(f"Total trainable parameters: {total_params:,}") |
|
|
|
|
| |
| |
| |
| logger.info("Creating dataloaders and lr_scheduler") |
|
|
| with open("./data/" + config.dataset.optimization_data + ".json", 'r') as f: |
| dataset_load = json.load(f) |
|
|
| prompt_list = [] |
| response_list = [] |
| step_map_list = [] |
| for x in dataset_load: |
| prompt_list.append(x["question"]) |
| answer = x["answer"] |
| |
| if isinstance(answer, list): |
| answer = answer[0] if len(answer) > 0 else "" |
| response_list.append(answer) |
| step_map_list.append(x["step_map"][0]) |
|
|
| input_ids_lm, _, start_pos, drop_num = uni_prompting((prompt_list, response_list)) |
|
|
| _, L = input_ids_lm.shape |
| L0 = start_pos |
| L1 = L - L0 |
| post_num = config.training.post_num |
|
|
| basic_block_attention = make_basic_block_attention(L0 + 2 * L1, start_pos, config.training.block_size) |
| basic_block_attention = basic_block_attention.cpu() |
| |
| |
| vocab_size = tokenizer.vocab_size |
| |
| extended_input_ids, p_mask, tok_idx_ext, labels = collect_training_data(config, input_ids_lm, start_pos, pad_id, mask_id, vocab_size, post_num, step_map_list=step_map_list) |
|
|
| step_map_list_expanded = step_map_list |
|
|
| dataset_lm = TrainDataset(extended_input_ids, p_mask, tok_idx_ext, labels, step_map_list_expanded) |
|
|
| total_batch_size_lm = config.training.batch_size_lm * accelerator.num_processes * config.training.gradient_accumulation_steps |
| num_update_steps_per_epoch = math.ceil(len(dataset_lm) / total_batch_size_lm) |
| num_train_epochs = config.training.num_train_epochs |
| max_train_steps = num_update_steps_per_epoch * num_train_epochs + 1 |
|
|
| lr_scheduler = get_scheduler( |
| config.lr_scheduler.scheduler, |
| optimizer=optimizer, |
| num_training_steps=max_train_steps, |
| num_warmup_steps=config.lr_scheduler.params.warmup_steps, |
| min_lr_scale=config.lr_scheduler.params.min_lr_scale |
| ) |
|
|
| |
| generator = None |
| if hasattr(config.training, 'seed') and config.training.seed is not None: |
| generator = torch.Generator() |
| generator.manual_seed(config.training.seed) |
|
|
| train_dataloader_lm = DataLoader( |
| dataset_lm, |
| batch_size=config.training.batch_size_lm, |
| sampler=None, |
| collate_fn=simple_collate, |
| num_workers=0, |
| shuffle=True, |
| generator=generator, |
| ) |
|
|
|
|
| |
| |
| |
| logger.info("Preparing model, optimizer and dataloaders") |
| model, optimizer, lr_scheduler, train_dataloader_lm = accelerator.prepare( |
| model, optimizer, lr_scheduler, train_dataloader_lm |
| ) |
|
|
| |
| |
| |
| logger.info("***** Running SDAR training (Full Fine-tuning) *****") |
| |
| logger.info(f" Num response = {len(dataset_load)}") |
| logger.info(f" Num training data = {len(dataset_lm)}") |
| logger.info(f" Num training steps = {max_train_steps}") |
| logger.info(f" Instantaneous batch size per device = {config.training.batch_size_lm}") |
| logger.info(f" Total train batch size (w. parallel, distributed & accumulation) = {total_batch_size_lm}") |
| logger.info(f" Gradient Accumulation steps = {config.training.gradient_accumulation_steps}") |
| |
| first_epoch = 0 |
| global_step = 0 |
| end = time.time() |
|
|
| from tqdm.auto import tqdm |
|
|
| for epoch in range(first_epoch, num_train_epochs): |
|
|
| model.train() |
| |
| progress_bar = tqdm( |
| train_dataloader_lm, |
| desc=f"Epoch {epoch+1}/{num_train_epochs}", |
| disable=not accelerator.is_local_main_process, |
| dynamic_ncols=True, |
| leave=True |
| ) |
| |
| |
| loss_accumulator = { |
| "loss": 0.0, |
| "loss_path_step_map": 0.0, |
| "loss_ddo": 0.0, |
| "loss_real": 0.0, |
| "loss_fake": 0.0, |
| "logp_target_real": 0.0, |
| "logp_ref_real": 0.0, |
| "logp_target_fake": 0.0, |
| "logp_ref_fake": 0.0, |
| } |
| |
| loss_count = { |
| "loss": 0, |
| "loss_path_step_map": 0, |
| "loss_ddo": 0, |
| "loss_real": 0, |
| "loss_fake": 0, |
| "logp_target_real": 0, |
| "logp_ref_real": 0, |
| "logp_target_fake": 0, |
| "logp_ref_fake": 0, |
| } |
| |
| for step, batch in enumerate(progress_bar, start=1): |
|
|
| extended_input_ids = batch["extended_input_ids"].to(accelerator.device) |
| p_mask = batch["p_mask"].to(accelerator.device) |
| tok_idx_ext = batch["tok_idx_ext"].to(accelerator.device) |
| labels = batch["labels"].to(accelerator.device) |
| |
| |
| with accelerator.accumulate(model): |
| optimizer.zero_grad() |
| with torch.no_grad(): |
| |
| reference_logp_real = compute_logp(model, ref_model, extended_input_ids, p_mask, tok_idx_ext, labels, start_pos, basic_block_attention, pad_id, reference_logits=True) |
| |
| reference_logp_fake, logits_x0_fake = compute_logp_fake(model, ref_model, extended_input_ids, p_mask, tok_idx_ext, labels, start_pos, basic_block_attention, pad_id, temperature=config.training.reference_temp, reference_logits=True, x_theta_fake=None) |
|
|
| if config.training.reference_temp == 0.0: |
| x_theta_fake = torch.argmax(logits_x0_fake, dim=-1) |
| else: |
| logits_with_noise = add_gumbel_noise(logits_x0_fake, temperature=config.training.reference_temp) |
| x_theta_fake = torch.argmax(logits_with_noise, dim=-1) |
|
|
| |
| target_logp_real = compute_logp(model, ref_model, extended_input_ids, p_mask, tok_idx_ext, labels, start_pos, basic_block_attention, pad_id, reference_logits=False) |
|
|
| |
| target_logp_fake, logits_x0_theta = compute_logp_fake(model, ref_model, extended_input_ids, p_mask, tok_idx_ext, labels, start_pos, basic_block_attention, pad_id, temperature=config.training.target_temp, reference_logits=False, x_theta_fake=x_theta_fake) |
|
|
|
|
| |
| loss, metrics = compute_loss(config, target_logp_real, target_logp_fake, reference_logp_real, reference_logp_fake) |
|
|
| |
| step_map = batch["step_map_list"].to(accelerator.device) |
| k_steps = getattr(config.training, "path_k_steps", 2) |
| lambda_path = getattr(config.training, "lambda_path", 0.05) |
| |
| |
| if step_map.dtype != torch.long: |
| step_map = step_map.long() |
| |
| L_path_step_map = compute_path_loss_from_step_map( |
| logits_x0_theta, labels, step_map, |
| config.training.block_size, k_steps, |
| mask_id, pad_id, start_pos |
| ) |
| |
| |
| loss = loss + lambda_path * L_path_step_map |
|
|
| loss_accumulator['loss'] += metrics['loss'] |
| loss_accumulator['loss_path_step_map'] += L_path_step_map.item() |
| loss_accumulator['loss_ddo'] += loss.item() |
| loss_accumulator['loss_real'] += metrics['loss_real'] |
| loss_accumulator['loss_fake'] += metrics['loss_fake'] |
| loss_accumulator['logp_target_real'] += metrics['logp_target_real'] |
| loss_accumulator['logp_ref_real'] += metrics['logp_ref_real'] |
| loss_accumulator['logp_target_fake'] += metrics['logp_target_fake'] |
| loss_accumulator['logp_ref_fake'] += metrics['logp_ref_fake'] |
|
|
| loss_count['loss'] += 1 |
| loss_count['loss_path_step_map'] += 1 |
| loss_count['loss_ddo'] += 1 |
| loss_count['loss_real'] += 1 |
| loss_count['loss_fake'] += 1 |
| loss_count['logp_target_real'] += 1 |
| loss_count['logp_ref_real'] += 1 |
| loss_count['logp_target_fake'] += 1 |
| loss_count['logp_ref_fake'] += 1 |
| |
| |
| accelerator.backward(loss) |
|
|
| if config.experiment.gradient_clipping and accelerator.sync_gradients: |
| accelerator.clip_grad_norm_(model.parameters(), config.experiment.max_grad_norm) |
|
|
| optimizer.step() |
| lr_scheduler.step() |
|
|
| |
| if (step + 1) % config.training.gradient_accumulation_steps == 0: |
| global_step += 1 |
| |
| |
| avg_losses = {} |
| for key in loss_accumulator.keys(): |
| if loss_count[key] > 0: |
| avg_losses[key] = loss_accumulator[key] / loss_count[key] |
| else: |
| avg_losses[key] = 0.0 |
| |
| |
| if accelerator.is_main_process: |
| grad_norm = model.get_global_grad_norm() if hasattr(model, 'get_global_grad_norm') else None |
| current_round = ((global_step - 1) // round_interval) + 1 if multi_round else 0 |
| total_rounds = max_train_steps // round_interval if multi_round else 0 |
| print("=" * 80) |
| if multi_round: |
| print(f"Round {current_round}/{total_rounds} | Epoch {epoch+1}/{num_train_epochs} | Global Step {global_step} | Local Step {step}/{len(train_dataloader_lm)}") |
| else: |
| print(f"Epoch {epoch+1}/{num_train_epochs} | Global Step {global_step} | Local Step {step}/{len(train_dataloader_lm)}") |
| print("-" * 80) |
| print(f"Learning Rate: {lr_scheduler.get_last_lr()[0]}") |
| print(f"Loss: {avg_losses['loss']:.4f}") |
| print(f"Loss Path Step Map: {avg_losses['loss_path_step_map']:.4f}") |
| print(f"Loss DDO: {avg_losses['loss_ddo']:.4f}") |
| print(f"Loss Real: {avg_losses['loss_real']:.4f}") |
| print(f"Loss Fake: {avg_losses['loss_fake']:.4f}") |
| print(f"Logp Target Real: {avg_losses['logp_target_real']:.4f}") |
| print(f"Logp Ref Real: {avg_losses['logp_ref_real']:.4f}") |
| print(f"Logp Target Fake: {avg_losses['logp_target_fake']:.4f}") |
| print(f"Logp Ref Fake: {avg_losses['logp_ref_fake']:.4f}") |
| print(f"Grad Norm: {grad_norm.item() if grad_norm is not None else 0.0:.4f}") |
| print("=" * 80) |
| |
| |
| if accelerator.is_main_process: |
| grad_norm = model.get_global_grad_norm() if hasattr(model, 'get_global_grad_norm') else None |
| current_round = ((global_step - 1) // round_interval) + 1 if multi_round else 0 |
| log_dict = { |
| "loss": avg_losses['loss'], |
| "loss_path_step_map": avg_losses['loss_path_step_map'], |
| "loss_ddo": avg_losses['loss_ddo'], |
| "loss_real": avg_losses['loss_real'], |
| "loss_fake": avg_losses['loss_fake'], |
| "logp_target_real": avg_losses['logp_target_real'], |
| "logp_ref_real": avg_losses['logp_ref_real'], |
| "logp_target_fake": avg_losses['logp_target_fake'], |
| "logp_ref_fake": avg_losses['logp_ref_fake'], |
| "grad_norm": grad_norm.item() if grad_norm is not None else 0.0, |
| "learning_rate": lr_scheduler.get_last_lr()[0], |
| "epoch": epoch + 1, |
| "global_step": global_step, |
| } |
| if multi_round: |
| log_dict["round"] = current_round |
| accelerator.log(log_dict, step=global_step) |
| |
| |
| progress_bar.set_postfix({ |
| 'loss': f"{avg_losses['loss']:.4f}", |
| 'loss_real': f"{avg_losses['loss_real']:.4f}", |
| 'loss_fake': f"{avg_losses['loss_fake']:.4f}", |
| }) |
| |
| |
| for key in loss_accumulator: |
| loss_accumulator[key] = 0.0 |
| loss_count[key] = 0 |
|
|
| |
| if global_step % config.experiment.get("save_every_steps", 128) == 0: |
| accelerator.wait_for_everyone() |
| logger.info(f"About to save checkpoint at global step {global_step}") |
| name = f"{config.model.optimized_name}-{epoch}-{global_step}" |
| save_checkpoint(model, tokenizer, config, accelerator, name, project_timestamp_dir) |
| logger.info(f"Saved checkpoint at global step {global_step}") |
| |
| |
| |
| |
| if multi_round and (global_step % round_interval == 0): |
| current_round = global_step // round_interval |
| accelerator.wait_for_everyone() |
| if accelerator.is_main_process: |
| logger.info("=" * 80) |
| logger.info(f"MULTI-ROUND REFINEMENT: Completing Round {current_round}") |
| logger.info(f"Updating reference model with trained model weights at global step {global_step}") |
| logger.info("=" * 80) |
| |
| |
| unwrapped_model = accelerator.unwrap_model(model) |
| copy_model_weights(unwrapped_model, ref_model) |
| |
| |
| name = f"{config.model.optimized_name}-round{current_round}-step{global_step}" |
| save_checkpoint(model, tokenizer, config, accelerator, name, project_timestamp_dir) |
| |
| if accelerator.is_main_process: |
| logger.info(f"Round {current_round} completed. Reference model updated for Round {current_round + 1}") |
| logger.info("=" * 80) |
| |
| accelerator.wait_for_everyone() |
|
|
| |
| save_checkpoint(model, tokenizer, config, accelerator, config.model.optimized_name, project_timestamp_dir) |
|
|
| accelerator.end_training() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|
|
|