| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import os |
| import json |
| import random |
| import gc |
| import inspect |
| from pathlib import Path |
| from typing import Dict, Any, List, Optional |
|
|
| import numpy as np |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
| from datasets import Dataset |
| from sklearn.metrics import mean_squared_error, r2_score |
| from scipy.stats import pearsonr |
|
|
| from transformers import ( |
| AutoTokenizer, |
| AutoConfig, |
| AutoModel, |
| AutoModelForCausalLM, |
| TrainingArguments, |
| Trainer, |
| TrainerCallback, |
| set_seed, |
| EvalPrediction, |
| ) |
| from transformers.data.data_collator import DataCollatorWithPadding |
| from transformers.modeling_outputs import SequenceClassifierOutput |
| from transformers import PreTrainedModel |
|
|
|
|
| |
| |
| |
| BASE_MODEL_PATH = "./checkpoint-388560" |
| TOKENIZER_PATH = "/opt/platform/regression_efficiency/checkpoint-5956" |
| TRAIN_JSON = "evenBetterDataFolded-tr.json" |
| VALID_JSON = "evenBetterDataFolded-vl.json" |
|
|
|
|
| |
| |
| |
| EXPERIMENT_NAME = "fullft_regression_sweep_stanardunnorm" |
| ROOT_OUT = Path(f"./qwen_regression_ckpt/{EXPERIMENT_NAME}") |
| TB_ROOT = Path(f"tensorboard/{EXPERIMENT_NAME}") |
| ROOT_OUT.mkdir(parents=True, exist_ok=True) |
| TB_ROOT.mkdir(parents=True, exist_ok=True) |
|
|
| PER_DEVICE_TRAIN_BATCH = 24 |
| PER_DEVICE_EVAL_BATCH = 2 |
| GRAD_ACCUM_STEPS = 1 |
|
|
| NUM_EPOCHS_CAP = 200 |
| EARLY_STOP_PATIENCE = 50 |
| EARLY_STOP_MIN_DELTA = 0.0 |
|
|
| |
| BEST_METRIC_KEY = "eval_pearson_r" |
| GREATER_IS_BETTER = True |
|
|
| |
| SWEEP_MODE = "random" |
| NUM_TRIALS = 12 |
| SEEDS = [42] |
|
|
| |
| |
| USE_BF16 = torch.cuda.is_available() and torch.cuda.is_bf16_supported() |
| DEFAULT_OPTIM = "adamw_torch_fused" |
|
|
|
|
| |
| |
| |
| _TA_SIG = inspect.signature(TrainingArguments.__init__) |
| _HAS_EVAL_STRATEGY = "eval_strategy" in _TA_SIG.parameters |
| _HAS_EVALUATION_STRATEGY = "evaluation_strategy" in _TA_SIG.parameters |
|
|
| def make_training_args(**kwargs): |
| """ |
| Make TrainingArguments across HF versions: |
| - some versions use eval_strategy, others evaluation_strategy |
| """ |
| |
| if "eval_strategy" in kwargs and not _HAS_EVAL_STRATEGY and _HAS_EVALUATION_STRATEGY: |
| kwargs["evaluation_strategy"] = kwargs.pop("eval_strategy") |
| if "evaluation_strategy" in kwargs and _HAS_EVAL_STRATEGY and not _HAS_EVALUATION_STRATEGY: |
| kwargs["eval_strategy"] = kwargs.pop("evaluation_strategy") |
| return TrainingArguments(**kwargs) |
|
|
|
|
| |
| |
| |
| def load_json_mapping(path: str) -> Dict[str, float]: |
| with open(path, "r", encoding="utf-8") as f: |
| d = json.load(f) |
| return {k: float(v) for k, v in d.items()} |
|
|
| train_map = load_json_mapping(TRAIN_JSON) |
| valid_map = load_json_mapping(VALID_JSON) |
|
|
| train_texts = list(train_map.keys()) |
| train_labels = [train_map[k] for k in train_texts] |
| valid_texts = list(valid_map.keys()) |
| valid_labels = [valid_map[k] for k in valid_texts] |
|
|
| |
| with open(TB_ROOT / "train.json", "w", encoding="utf-8") as f: |
| json.dump(train_map, f, indent=2) |
| with open(TB_ROOT / "valid.json", "w", encoding="utf-8") as f: |
| json.dump(valid_map, f, indent=2) |
|
|
| print("Train size:", len(train_texts), " Valid size:", len(valid_texts)) |
| print("Train labels stats:", |
| f"mean={np.mean(train_labels):.6f}", |
| f"std={np.std(train_labels):.6f}", |
| f"min={np.min(train_labels):.6f}", |
| f"max={np.max(train_labels):.6f}") |
| baseline_mse = float(np.mean((np.array(train_labels) - np.mean(train_labels))**2)) |
| print("Baseline MSE (predict train mean):", baseline_mse) |
|
|
| tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_PATH, use_fast=True) |
| |
| if tokenizer.pad_token is None: |
| tokenizer.pad_token = tokenizer.eos_token |
|
|
| train_raw = Dataset.from_dict({"text": train_texts, "labels": train_labels}) |
| valid_raw = Dataset.from_dict({"text": valid_texts, "labels": valid_labels}) |
|
|
| def tok(batch): |
| return tokenizer(batch["text"], truncation=True) |
|
|
| train_ds = train_raw.map(tok, batched=True, remove_columns=["text"]) |
| valid_ds = valid_raw.map(tok, batched=True, remove_columns=["text"]) |
| train_ds.set_format(type="torch") |
| valid_ds.set_format(type="torch") |
|
|
| data_collator = DataCollatorWithPadding(tokenizer=tokenizer, pad_to_multiple_of=8, return_tensors="pt") |
|
|
|
|
| |
| |
| |
| class LastTokenPooling(nn.Module): |
| def forward(self, hidden_states: torch.Tensor, attention_mask: Optional[torch.Tensor] = None): |
| if attention_mask is None: |
| return hidden_states[:, -1, :] |
| B, T, H = hidden_states.size() |
| |
| if attention_mask[:, -1].sum().item() == B: |
| return hidden_states[:, -1, :] |
| |
| idx = (attention_mask.sum(dim=1).long() - 1).clamp(min=0) |
| idx = idx.view(B, 1, 1).expand(-1, 1, H) |
| return hidden_states.gather(1, idx).squeeze(1) |
|
|
| class RegressionHead(nn.Module): |
| def __init__(self, hidden_size: int, head_dropout: float): |
| super().__init__() |
| self.ln = nn.LayerNorm(hidden_size) |
| self.drop = nn.Dropout(head_dropout) |
| self.out = nn.Linear(hidden_size, 1) |
|
|
| def forward(self, x: torch.Tensor) -> torch.Tensor: |
| x = self.ln(x) |
| x = self.drop(x) |
| return self.out(x).squeeze(-1) |
|
|
| def robust_set_dropout(config, p_hidden: float, p_attn: float, layerdrop: float): |
| |
| hidden_fields = [ |
| "hidden_dropout_prob", "hidden_dropout", "dropout", |
| "emb_dropout", "resid_pdrop", "classifier_dropout", |
| ] |
| attn_fields = [ |
| "attention_probs_dropout_prob", "attention_dropout", |
| "attn_dropout", "attn_pdrop", |
| ] |
| for f in hidden_fields: |
| if hasattr(config, f): |
| setattr(config, f, float(p_hidden)) |
| for f in attn_fields: |
| if hasattr(config, f): |
| setattr(config, f, float(p_attn)) |
| if hasattr(config, "layerdrop"): |
| setattr(config, "layerdrop", float(layerdrop)) |
|
|
| def load_backbone(base_model_path: str, config: AutoConfig): |
| dtype = torch.bfloat16 if USE_BF16 else (torch.float16 if USE_FP16 else None) |
| try: |
| return AutoModel.from_pretrained( |
| base_model_path, |
| config=config, |
| torch_dtype=dtype, |
| device_map=None, |
| ) |
| except Exception as e: |
| print("[warn] AutoModel load failed, falling back to AutoModelForCausalLM().model") |
| lm = AutoModelForCausalLM.from_pretrained( |
| base_model_path, |
| config=config, |
| torch_dtype=dtype, |
| device_map=None, |
| ) |
| if hasattr(lm, "model"): |
| return lm.model |
| if hasattr(lm, "transformer"): |
| return lm.transformer |
| raise RuntimeError("Could not locate backbone module on LM model.") from e |
|
|
| class QwenForRegression(PreTrainedModel): |
| config_class = AutoConfig |
| base_model_prefix = "backbone" |
|
|
| def __init__(self, config: AutoConfig, base_model_path: str, head_dropout: float): |
| super().__init__(config) |
| self.backbone = load_backbone(base_model_path, config) |
| self.pool = LastTokenPooling() |
| self.regression_head = RegressionHead(config.hidden_size, head_dropout=head_dropout) |
|
|
| |
| for p in self.parameters(): |
| p.requires_grad = True |
|
|
| def gradient_checkpointing_enable(self, **kwargs): |
| if hasattr(self.backbone, "gradient_checkpointing_enable"): |
| self.backbone.gradient_checkpointing_enable(**kwargs) |
|
|
| def gradient_checkpointing_disable(self, **kwargs): |
| if hasattr(self.backbone, "gradient_checkpointing_disable"): |
| self.backbone.gradient_checkpointing_disable(**kwargs) |
|
|
| def forward(self, input_ids=None, attention_mask=None, labels=None, **kwargs): |
| out = self.backbone(input_ids=input_ids, attention_mask=attention_mask, return_dict=True) |
| pooled = self.pool(out.last_hidden_state, attention_mask) |
| logits = self.regression_head(pooled) |
| loss = None |
| if labels is not None: |
| loss = F.mse_loss(logits.float(), labels.float()) |
| return SequenceClassifierOutput(loss=loss, logits=logits) |
|
|
| def save_pretrained(self, save_directory: str, state_dict=None, **kwargs): |
| """ |
| Save config + backbone (HF-style) + regression head separately. |
| Works with Trainer saving. |
| """ |
| os.makedirs(save_directory, exist_ok=True) |
| self.config.save_pretrained(save_directory) |
|
|
| |
| if state_dict is None: |
| state_dict = self.state_dict() |
|
|
| backbone_sd = {k[len("backbone."):]: v for k, v in state_dict.items() if k.startswith("backbone.")} |
| head_sd = {k[len("regression_head."):]: v for k, v in state_dict.items() if k.startswith("regression_head.")} |
|
|
| |
| self.backbone.save_pretrained(save_directory, state_dict=backbone_sd, **kwargs) |
| |
| torch.save(head_sd, os.path.join(save_directory, "regression_head.pt")) |
|
|
|
|
| |
| |
| |
| def safe_pearson(x: np.ndarray, y: np.ndarray) -> float: |
| x = np.asarray(x).reshape(-1) |
| y = np.asarray(y).reshape(-1) |
| if x.size < 2: |
| return 0.0 |
| if np.std(x) < 1e-12 or np.std(y) < 1e-12: |
| return 0.0 |
| r, _ = pearsonr(x, y) |
| return float(r) |
|
|
| def compute_metrics(eval_pred: EvalPrediction) -> Dict[str, float]: |
| preds = np.asarray(eval_pred.predictions).reshape(-1) |
| labels = np.asarray(eval_pred.label_ids).reshape(-1) |
|
|
| mse = mean_squared_error(labels, preds) |
| r2 = r2_score(labels, preds) |
| pr = safe_pearson(preds, labels) |
| return {"mse": float(mse), "r2": float(r2), "pearson_r": float(pr)} |
|
|
|
|
| |
| |
| |
| class EarlyStopNoSaveCallback(TrainerCallback): |
| """ |
| Early stopping WITHOUT requiring: |
| - metric_for_best_model |
| - load_best_model_at_end |
| - checkpoint saving |
| |
| It just stops training when eval metric stops improving. |
| """ |
| def __init__(self, metric_key: str, patience: int, greater_is_better: bool, min_delta: float = 0.0): |
| self.metric_key = metric_key |
| self.patience = int(patience) |
| self.greater_is_better = bool(greater_is_better) |
| self.min_delta = float(min_delta) |
|
|
| self.best = None |
| self.bad_count = 0 |
|
|
| def on_evaluate(self, args, state, control, metrics=None, **kwargs): |
| if metrics is None: |
| return control |
| if self.metric_key not in metrics: |
| |
| return control |
|
|
| val = float(metrics[self.metric_key]) |
| if self.best is None: |
| self.best = val |
| self.bad_count = 0 |
| return control |
|
|
| improved = (val - self.best) > self.min_delta if self.greater_is_better else (self.best - val) > self.min_delta |
| if improved: |
| self.best = val |
| self.bad_count = 0 |
| else: |
| self.bad_count += 1 |
| if self.bad_count >= self.patience: |
| control.should_training_stop = True |
| return control |
|
|
|
|
| |
| |
| |
| def sample_hparams(rng: random.Random) -> Dict[str, Any]: |
| |
| return { |
| "learning_rate": rng.choice([1e-5, 2e-5, 3e-5, 5e-5, 8e-5]), |
| "weight_decay": rng.choice([0.0, 0.01, 0.03, 0.05, 0.1]), |
| "hidden_dropout": rng.choice([0.0, 0.1, 0.2, 0.3]), |
| "attn_dropout": rng.choice([0.0, 0.1, 0.2, 0.3]), |
| "layerdrop": rng.choice([0.0, 0.05, 0.1, 0.2]), |
| "head_dropout": rng.choice([0.0, 0.1, 0.2, 0.3, 0.5]), |
| "max_grad_norm": rng.choice([0.5, 1.0, 2.0]), |
| "warmup_ratio": rng.choice([0.0, 0.03, 0.05, 0.1]), |
| "lr_scheduler_type": rng.choice(["cosine", "cosine_with_restarts"]), |
| "num_cycles": rng.choice([1, 2, 4]), |
| } |
|
|
| def grid_hparams() -> List[Dict[str, Any]]: |
| grid = [] |
| for lr in [2e-5, 3e-5, 5e-5]: |
| for wd in [0.0, 0.03, 0.1]: |
| for dp in [0.1, 0.2, 0.3]: |
| for head_dp in [0.1, 0.3]: |
| grid.append({ |
| "learning_rate": lr, |
| "weight_decay": wd, |
| "hidden_dropout": dp, |
| "attn_dropout": dp, |
| "layerdrop": 0.0, |
| "head_dropout": head_dp, |
| "max_grad_norm": 1.0, |
| "warmup_ratio": 0.05, |
| "lr_scheduler_type": "cosine", |
| "num_cycles": 1, |
| }) |
| return grid |
|
|
| HP_LIST = grid_hparams() if SWEEP_MODE == "grid" else [sample_hparams(random.Random(1234 + i)) for i in range(NUM_TRIALS)] |
|
|
|
|
| |
| |
| |
| def cleanup(): |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
|
|
| |
| |
| |
| def run_trial(trial_dir: Path, tb_dir: Path, h: Dict[str, Any], seed: int) -> Dict[str, Any]: |
| set_seed(seed) |
|
|
| config = AutoConfig.from_pretrained(BASE_MODEL_PATH) |
| robust_set_dropout(config, h["hidden_dropout"], h["attn_dropout"], h["layerdrop"]) |
|
|
| model = QwenForRegression(config=config, base_model_path=BASE_MODEL_PATH, head_dropout=h["head_dropout"]) |
|
|
| args = make_training_args( |
| output_dir=str(trial_dir), |
| per_device_train_batch_size=PER_DEVICE_TRAIN_BATCH, |
| per_device_eval_batch_size=PER_DEVICE_EVAL_BATCH, |
| gradient_accumulation_steps=GRAD_ACCUM_STEPS, |
| num_train_epochs=NUM_EPOCHS_CAP, |
| eval_strategy="epoch", |
| save_strategy="no", |
| logging_dir=str(tb_dir), |
| logging_steps=1, |
| report_to=["tensorboard"], |
| learning_rate=h["learning_rate"], |
| weight_decay=h["weight_decay"], |
| warmup_ratio=h["warmup_ratio"], |
| lr_scheduler_type=h["lr_scheduler_type"], |
| lr_scheduler_kwargs={"num_cycles": h["num_cycles"]} if h["lr_scheduler_type"] == "cosine_with_restarts" else {}, |
| bf16=USE_BF16, |
| fp16=USE_FP16, |
| gradient_checkpointing=True, |
| gradient_checkpointing_kwargs={"use_reentrant": False}, |
| max_grad_norm=h["max_grad_norm"], |
| dataloader_pin_memory=True, |
| remove_unused_columns=False, |
| optim=DEFAULT_OPTIM, |
| seed=seed, |
| data_seed=seed, |
| |
| ) |
|
|
| trainer = Trainer( |
| model=model, |
| args=args, |
| train_dataset=train_ds, |
| eval_dataset=valid_ds, |
| tokenizer=tokenizer, |
| data_collator=data_collator, |
| compute_metrics=compute_metrics, |
| callbacks=[EarlyStopNoSaveCallback( |
| metric_key=BEST_METRIC_KEY, |
| patience=EARLY_STOP_PATIENCE, |
| greater_is_better=GREATER_IS_BETTER, |
| min_delta=EARLY_STOP_MIN_DELTA, |
| )], |
| ) |
|
|
| train_result = trainer.train() |
| eval_metrics = trainer.evaluate() |
|
|
| out = { |
| "seed": int(seed), |
| "hparams": dict(h), |
| "train_metrics": {k: float(v) for k, v in train_result.metrics.items()}, |
| "eval_metrics": {k: float(v) for k, v in eval_metrics.items()}, |
| } |
|
|
| del trainer, model |
| cleanup() |
| return out |
|
|
|
|
| |
| |
| |
| results_file = ROOT_OUT / "sweep_results.jsonl" |
| best = None |
|
|
| trial_id = 0 |
| for h in HP_LIST: |
| for seed in SEEDS: |
| trial_dir = ROOT_OUT / "trials" / f"trial_{trial_id:03d}_seed{seed}" |
| tb_dir = TB_ROOT / "trials" / f"trial_{trial_id:03d}_seed{seed}" |
| trial_dir.mkdir(parents=True, exist_ok=True) |
| tb_dir.mkdir(parents=True, exist_ok=True) |
|
|
| print(f"\n=== TRIAL {trial_id:03d} seed={seed} ===") |
| print(json.dumps(h, indent=2)) |
|
|
| record = run_trial(trial_dir, tb_dir, h, seed) |
|
|
| with open(results_file, "a", encoding="utf-8") as f: |
| f.write(json.dumps({"trial_id": trial_id, **record}) + "\n") |
|
|
| |
| score = record["eval_metrics"].get(BEST_METRIC_KEY, None) |
| if score is None: |
| |
| score = -record["eval_metrics"].get("eval_loss", 1e30) |
|
|
| score = float(score) |
| is_better = (best is None) or ((score > best["score"]) if GREATER_IS_BETTER else (score < best["score"])) |
|
|
| if is_better: |
| best = {"score": score, "trial_id": trial_id, "record": record} |
| print(f"--> NEW BEST: {BEST_METRIC_KEY} = {score:.6f}") |
|
|
| trial_id += 1 |
|
|
| if best is None: |
| raise RuntimeError("No trials ran.") |
|
|
| print("\n====================") |
| print("BEST TRIAL SUMMARY") |
| print("====================") |
| print(json.dumps( |
| { |
| "trial_id": best["trial_id"], |
| "score": best["score"], |
| "seed": best["record"]["seed"], |
| "hparams": best["record"]["hparams"], |
| "eval_metrics": best["record"]["eval_metrics"], |
| }, |
| indent=2, |
| )) |
|
|
|
|
| |
| |
| |
| final_dir = ROOT_OUT / "final" / "model" |
| final_tb = TB_ROOT / "final" |
| final_dir.mkdir(parents=True, exist_ok=True) |
| final_tb.mkdir(parents=True, exist_ok=True) |
|
|
| best_h = best["record"]["hparams"] |
| best_seed = int(best["record"]["seed"]) |
| set_seed(best_seed) |
|
|
| final_config = AutoConfig.from_pretrained(BASE_MODEL_PATH) |
| robust_set_dropout(final_config, best_h["hidden_dropout"], best_h["attn_dropout"], best_h["layerdrop"]) |
| final_model = QwenForRegression(config=final_config, base_model_path=BASE_MODEL_PATH, head_dropout=best_h["head_dropout"]) |
|
|
| |
| |
| |
| metric_for_best = "pearson_r" |
|
|
| final_args = make_training_args( |
| output_dir=str(final_dir), |
| per_device_train_batch_size=PER_DEVICE_TRAIN_BATCH, |
| per_device_eval_batch_size=PER_DEVICE_EVAL_BATCH, |
| gradient_accumulation_steps=GRAD_ACCUM_STEPS, |
| num_train_epochs=NUM_EPOCHS_CAP, |
| eval_strategy="epoch", |
| save_strategy="epoch", |
| save_total_limit=2, |
| load_best_model_at_end=True, |
| metric_for_best_model=metric_for_best, |
| greater_is_better=GREATER_IS_BETTER, |
| logging_dir=str(final_tb), |
| logging_steps=1, |
| report_to=["tensorboard"], |
| learning_rate=best_h["learning_rate"], |
| weight_decay=best_h["weight_decay"], |
| warmup_ratio=best_h["warmup_ratio"], |
| lr_scheduler_type=best_h["lr_scheduler_type"], |
| lr_scheduler_kwargs={"num_cycles": best_h["num_cycles"]} if best_h["lr_scheduler_type"] == "cosine_with_restarts" else {}, |
| bf16=USE_BF16, |
| fp16=USE_FP16, |
| gradient_checkpointing=True, |
| gradient_checkpointing_kwargs={"use_reentrant": False}, |
| max_grad_norm=best_h["max_grad_norm"], |
| dataloader_pin_memory=True, |
| remove_unused_columns=False, |
| optim=DEFAULT_OPTIM, |
| seed=best_seed, |
| data_seed=best_seed, |
| ) |
|
|
| final_trainer = Trainer( |
| model=final_model, |
| args=final_args, |
| train_dataset=train_ds, |
| eval_dataset=valid_ds, |
| tokenizer=tokenizer, |
| data_collator=data_collator, |
| compute_metrics=compute_metrics, |
| |
| callbacks=[EarlyStopNoSaveCallback( |
| metric_key=BEST_METRIC_KEY, |
| patience=EARLY_STOP_PATIENCE, |
| greater_is_better=GREATER_IS_BETTER, |
| min_delta=EARLY_STOP_MIN_DELTA, |
| )], |
| ) |
|
|
| final_trainer.train() |
| final_metrics = final_trainer.evaluate() |
|
|
| print("\nFINAL EVAL METRICS:") |
| print(json.dumps({k: float(v) for k, v in final_metrics.items()}, indent=2)) |
|
|
| |
| tokenizer.save_pretrained(str(final_dir)) |
|
|
| |
| with open(ROOT_OUT / "best_hparams.json", "w", encoding="utf-8") as f: |
| json.dump(best_h, f, indent=2) |
|
|
| print("\nSaved final model to:", str(final_dir)) |
| print("Sweep results jsonl:", str(results_file)) |
| print("Tensorboard root:", str(TB_ROOT)) |
|
|