| |
| """Overnight ablation study runner. |
| |
| Runs 4 ablation training experiments sequentially, then evaluates all models |
| in a single comprehensive HTML report. |
| |
| Ablations (all from LaionBox v0.1-wip, 2 epochs, same hyperparameters): |
| A: naturalness + quality MLP |
| B: naturalness + centroid |
| C: naturalness + quality MLP + speaker similarity |
| D: naturalness + speaker similarity |
| |
| After training, generates audio for the best-flow and best-naturalness |
| checkpoint from each ablation, scores all models (including baselines), |
| and produces a combined HTML comparison. |
| """ |
|
|
| import json |
| import os |
| import signal |
| import subprocess |
| import sys |
| import time |
|
|
| PYTHON = "/home/deployer/miniconda3/envs/ml-general/bin/python" |
| VAP_DIR = "/home/deployer/laion/Voice-Acting-Pipeline" |
| TRAIN_SCRIPT = os.path.join(VAP_DIR, "scripts", "dramabox_finetune_train_multi_aux.py") |
| EVAL_SCRIPT = os.path.join(VAP_DIR, "scripts", "run_ablation_eval.py") |
|
|
| ABLATIONS = [ |
| { |
| "name": "A_nat_quality", |
| "config": "configs/ablation_nat_quality.yaml", |
| "output_dir": "finetune_output/ablation_nat_quality", |
| "desc_prefix": "Nat+Quality", |
| "losses": "naturalness + quality_mlp", |
| }, |
| { |
| "name": "B_nat_centroid", |
| "config": "configs/ablation_nat_centroid.yaml", |
| "output_dir": "finetune_output/ablation_nat_centroid", |
| "desc_prefix": "Nat+Centroid", |
| "losses": "naturalness + centroid", |
| }, |
| { |
| "name": "C_nat_quality_speaker", |
| "config": "configs/ablation_nat_quality_speaker.yaml", |
| "output_dir": "finetune_output/ablation_nat_quality_speaker", |
| "desc_prefix": "Nat+Quality+Speaker", |
| "losses": "naturalness + quality_mlp + speaker_sim", |
| }, |
| { |
| "name": "D_nat_speaker", |
| "config": "configs/ablation_nat_speaker.yaml", |
| "output_dir": "finetune_output/ablation_nat_speaker", |
| "desc_prefix": "Nat+Speaker", |
| "losses": "naturalness + speaker_sim", |
| }, |
| ] |
|
|
|
|
| def log(msg): |
| ts = time.strftime("%Y-%m-%d %H:%M:%S") |
| line = f"[{ts}] {msg}" |
| print(line, flush=True) |
|
|
|
|
| def find_best_checkpoints(output_dir): |
| """Parse metrics.jsonl to find best flow loss and best naturalness checkpoints.""" |
| metrics_path = os.path.join(VAP_DIR, output_dir, "metrics.jsonl") |
| if not os.path.exists(metrics_path): |
| log(f" WARNING: {metrics_path} not found") |
| return None, None |
|
|
| best_flow_loss = float("inf") |
| best_flow_step = None |
| best_nat = float("-inf") |
| best_nat_step = None |
|
|
| with open(metrics_path) as f: |
| for line in f: |
| line = line.strip() |
| if not line: |
| continue |
| try: |
| m = json.loads(line) |
| except json.JSONDecodeError: |
| continue |
| step = m.get("step", 0) |
| flow = m.get("flow_loss", m.get("loss", float("inf"))) |
| nat = m.get("naturalness_reward", float("-inf")) |
|
|
| if flow < best_flow_loss: |
| best_flow_loss = flow |
| best_flow_step = step |
| if nat is not None and nat > best_nat: |
| best_nat = nat |
| best_nat_step = step |
|
|
| log(f" Best flow: step {best_flow_step} (loss={best_flow_loss:.4f})") |
| log(f" Best nat: step {best_nat_step} (nat={best_nat:.4f})") |
|
|
| |
| def find_ckpt(step): |
| if step is None: |
| return None, None |
| |
| for offset in [0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5]: |
| candidate_step = (step + offset * 10) if offset != 0 else step |
| |
| fname = f"lora_step_{candidate_step:05d}.safetensors" |
| path = os.path.join(VAP_DIR, output_dir, fname) |
| if os.path.exists(path): |
| return path, candidate_step |
| |
| fname = f"lora_step_{candidate_step:05d}.safetensors" |
| path = os.path.join(VAP_DIR, output_dir, fname) |
| if os.path.exists(path): |
| return path, candidate_step |
| |
| for epoch in [1, 2]: |
| path = os.path.join(VAP_DIR, output_dir, f"lora_epoch{epoch}.safetensors") |
| if os.path.exists(path): |
| return path, f"epoch{epoch}" |
| return None, None |
|
|
| |
| def snap_step(step): |
| if step is None: |
| return None |
| return round(step / 10) * 10 |
|
|
| flow_path, flow_step = find_ckpt(snap_step(best_flow_step)) |
| nat_path, nat_step = find_ckpt(snap_step(best_nat_step)) |
|
|
| if flow_path: |
| log(f" Flow checkpoint: {os.path.basename(flow_path)}") |
| else: |
| log(f" WARNING: No flow checkpoint found near step {best_flow_step}") |
|
|
| if nat_path: |
| log(f" Nat checkpoint: {os.path.basename(nat_path)}") |
| else: |
| log(f" WARNING: No nat checkpoint found near step {best_nat_step}") |
|
|
| return (flow_path, flow_step, best_flow_loss), (nat_path, nat_step, best_nat) |
|
|
|
|
| def run_training(ablation): |
| """Run a single ablation training.""" |
| name = ablation["name"] |
| config = ablation["config"] |
| log_file = f"/tmp/ablation_{name}.log" |
|
|
| log(f"{'='*70}") |
| log(f"STARTING ABLATION: {name} ({ablation['losses']})") |
| log(f"Config: {config}") |
| log(f"Log: {log_file}") |
| log(f"{'='*70}") |
|
|
| cmd = [ |
| "env", "-u", "LD_LIBRARY_PATH", |
| "accelerate", "launch", "--num_processes=8", |
| TRAIN_SCRIPT, |
| "--config", config, |
| ] |
|
|
| env = os.environ.copy() |
| env.pop("LD_LIBRARY_PATH", None) |
| |
| env["PATH"] = "/home/deployer/miniconda3/envs/ml-general/bin:" + env.get("PATH", "") |
|
|
| with open(log_file, "w") as lf: |
| proc = subprocess.Popen( |
| cmd, stdout=lf, stderr=subprocess.STDOUT, |
| cwd=VAP_DIR, env=env, |
| ) |
|
|
| log(f"Training PID: {proc.pid}") |
|
|
| |
| t0 = time.time() |
| while True: |
| ret = proc.poll() |
| if ret is not None: |
| break |
| elapsed = time.time() - t0 |
| |
| if int(elapsed) % 300 == 0 and elapsed > 10: |
| |
| status_path = os.path.join(VAP_DIR, ablation["output_dir"], "status.json") |
| if os.path.exists(status_path): |
| try: |
| with open(status_path) as sf: |
| status = json.load(sf) |
| step = status.get("step", "?") |
| total = status.get("total_steps", "?") |
| loss = status.get("flow_loss", status.get("loss", "?")) |
| eta = status.get("eta_sec", 0) |
| eta_m = int(eta // 60) if isinstance(eta, (int, float)) else "?" |
| log(f" Progress: step {step}/{total}, loss={loss}, ETA={eta_m}m") |
| except Exception: |
| pass |
| time.sleep(10) |
|
|
| elapsed_min = (time.time() - t0) / 60 |
| if ret == 0: |
| log(f"Training {name} COMPLETED in {elapsed_min:.1f} min (exit code 0)") |
| else: |
| log(f"Training {name} FAILED with exit code {ret} after {elapsed_min:.1f} min") |
| log(f" Check log: {log_file}") |
|
|
| return ret == 0 |
|
|
|
|
| def write_eval_models_json(all_models, path): |
| """Write the models dict as JSON for the eval script.""" |
| with open(path, "w") as f: |
| json.dump(all_models, f, indent=2) |
| log(f"Wrote {len(all_models)} models to {path}") |
|
|
|
|
| def upload_to_hf(abl_name, output_dir, flow_info, nat_info): |
| """Upload best checkpoints + metrics to HF.""" |
| try: |
| from huggingface_hub import HfApi |
| api = HfApi(token="HF_TOKEN_REDACTED") |
| repo = "TTS-AGI/laionbox-ablation-checkpoints" |
|
|
| uploads = [] |
| metrics_path = os.path.join(VAP_DIR, output_dir, "metrics.jsonl") |
| if os.path.exists(metrics_path): |
| uploads.append((metrics_path, f"{abl_name}/metrics.jsonl")) |
|
|
| if flow_info and flow_info[0]: |
| path, step, _ = flow_info |
| uploads.append((path, f"{abl_name}/best_flow_step{step}.safetensors")) |
| if nat_info and nat_info[0]: |
| path, step, _ = nat_info |
| uploads.append((path, f"{abl_name}/best_nat_step{step}.safetensors")) |
|
|
| for local, remote in uploads: |
| if os.path.exists(local): |
| sz = os.path.getsize(local) / 1024 / 1024 |
| log(f" HF upload: {remote} ({sz:.0f} MB)") |
| api.upload_file(path_or_fileobj=local, path_in_repo=remote, repo_id=repo) |
| log(f" HF upload complete for {abl_name}") |
| except Exception as e: |
| log(f" HF upload FAILED for {abl_name}: {e}") |
|
|
|
|
| def run_eval(models_json_path, output_dir): |
| """Run the comprehensive evaluation.""" |
| log(f"{'='*70}") |
| log("STARTING COMPREHENSIVE EVALUATION") |
| log(f"{'='*70}") |
|
|
| log_file = "/tmp/ablation_eval.log" |
|
|
| cmd = [ |
| "env", "-u", "LD_LIBRARY_PATH", |
| PYTHON, EVAL_SCRIPT, |
| "--models-json", models_json_path, |
| "--num-gpus", "8", |
| "--output-dir", output_dir, |
| ] |
|
|
| env = os.environ.copy() |
| env.pop("LD_LIBRARY_PATH", None) |
|
|
| with open(log_file, "w") as lf: |
| proc = subprocess.Popen( |
| cmd, stdout=lf, stderr=subprocess.STDOUT, |
| cwd=VAP_DIR, env=env, |
| ) |
|
|
| log(f"Eval PID: {proc.pid}, log: {log_file}") |
|
|
| proc.wait() |
| elapsed_min = proc.returncode |
| if proc.returncode == 0: |
| log("Evaluation COMPLETED successfully") |
| else: |
| log(f"Evaluation FAILED with exit code {proc.returncode}") |
| log(f" Check log: {log_file}") |
|
|
| return proc.returncode == 0 |
|
|
|
|
| def main(): |
| log("="*70) |
| log("ABLATION STUDY: Auxiliary Loss Comparison") |
| log("4 training runs + comprehensive evaluation") |
| log("="*70) |
| log("") |
| log("Ablations:") |
| for i, abl in enumerate(ABLATIONS): |
| log(f" {abl['name']}: {abl['losses']}") |
| log("") |
|
|
| |
| results = {} |
| for abl in ABLATIONS: |
| |
| metrics_path = os.path.join(VAP_DIR, abl["output_dir"], "metrics.jsonl") |
| if os.path.exists(metrics_path): |
| with open(metrics_path) as f: |
| n_lines = sum(1 for _ in f) |
| if n_lines >= 20: |
| log(f"SKIPPING {abl['name']}: already has {n_lines} metric entries") |
| log(f"Finding best checkpoints for {abl['name']}...") |
| flow_info, nat_info = find_best_checkpoints(abl["output_dir"]) |
| results[abl["name"]] = { |
| "ablation": abl, |
| "flow": flow_info, |
| "nat": nat_info, |
| } |
| log("") |
| continue |
|
|
| success = run_training(abl) |
| if success: |
| log(f"\nFinding best checkpoints for {abl['name']}...") |
| flow_info, nat_info = find_best_checkpoints(abl["output_dir"]) |
| results[abl["name"]] = { |
| "ablation": abl, |
| "flow": flow_info, |
| "nat": nat_info, |
| } |
| |
| log(f"Uploading {abl['name']} checkpoints to HF...") |
| upload_to_hf(abl["name"], abl["output_dir"], flow_info, nat_info) |
| else: |
| log(f"\nWARNING: {abl['name']} failed, skipping checkpoint extraction") |
| results[abl["name"]] = None |
| log("") |
|
|
| |
| log("\n" + "="*70) |
| log("TRAINING PHASE COMPLETE — BUILDING EVALUATION") |
| log("="*70) |
|
|
| models = { |
| "vanilla": { |
| "name": "Vanilla DramaBox", |
| "lora": None, |
| "desc": "Base DramaBox model without any fine-tuning", |
| }, |
| "laionbox_v01": { |
| "name": "LaionBox v0.1-wip", |
| "lora": "/home/deployer/.cache/huggingface/hub/models--laion--laionbox-v0.1-wip/snapshots/66176d2a653a013a7b71c1ccb7a7a4d4cf514b0d/lora_epoch5.safetensors", |
| "desc": "Previous best LoRA (5-epoch diff reward, DramaBox+Emolia data)", |
| }, |
| "nat_only_best_flow": { |
| "name": "Nat-Only Best-Flow (s160)", |
| "lora": os.path.join(VAP_DIR, "finetune_output/nat_only_2ep/lora_step_00160.safetensors"), |
| "desc": "Naturalness-only (CLAP-7B), best flow=0.528 @s160", |
| }, |
| "nat_only_best_nat": { |
| "name": "Nat-Only Best-Nat (s190)", |
| "lora": os.path.join(VAP_DIR, "finetune_output/nat_only_2ep/lora_step_00190.safetensors"), |
| "desc": "Naturalness-only (CLAP-7B), best nat=0.111 @s190", |
| }, |
| } |
|
|
| for abl_name, res in results.items(): |
| if res is None: |
| continue |
| abl = res["ablation"] |
| prefix = abl["desc_prefix"] |
| losses_short = abl["losses"] |
|
|
| if res["flow"] and res["flow"][0]: |
| path, step, loss = res["flow"] |
| key = f"{abl_name}_best_flow" |
| models[key] = { |
| "name": f"{prefix} Best-Flow (s{step})", |
| "lora": path, |
| "desc": f"{losses_short}, best flow={loss:.4f} @s{step}", |
| } |
|
|
| if res["nat"] and res["nat"][0]: |
| path, step, nat_score = res["nat"] |
| key = f"{abl_name}_best_nat" |
| models[key] = { |
| "name": f"{prefix} Best-Nat (s{step})", |
| "lora": path, |
| "desc": f"{losses_short}, best nat={nat_score:.4f} @s{step}", |
| } |
|
|
| log(f"\nTotal models for evaluation: {len(models)}") |
| for key, minfo in models.items(): |
| log(f" {key}: {minfo['name']}") |
|
|
| |
| models_json = os.path.join(VAP_DIR, "ablation_eval_models.json") |
| write_eval_models_json(models, models_json) |
|
|
| |
| eval_output = os.path.join(VAP_DIR, "ablation_eval") |
| success = run_eval(models_json, eval_output) |
|
|
| |
| log("\n" + "="*70) |
| log("ABLATION STUDY COMPLETE") |
| log("="*70) |
|
|
| if success: |
| report_path = os.path.join(eval_output, "eval_report.html") |
| log(f"HTML report: {report_path}") |
| log("Serve with: python -m http.server 8780 --directory " + eval_output) |
| else: |
| log("Evaluation failed — check /tmp/ablation_eval.log") |
|
|
| |
| log("\nTraining Summary:") |
| for abl_name, res in results.items(): |
| if res is None: |
| log(f" {abl_name}: FAILED") |
| continue |
| flow_str = f"flow={res['flow'][2]:.4f}@s{res['flow'][1]}" if res["flow"] and res["flow"][0] else "N/A" |
| nat_str = f"nat={res['nat'][2]:.4f}@s{res['nat'][1]}" if res["nat"] and res["nat"][0] else "N/A" |
| log(f" {abl_name}: {flow_str}, {nat_str}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|