Download code/scripts/run_ablation_overnight.py from TTS-AGI/laionbox-ablation-checkpoints: direct link, hf CLI and curl.
- Browser
- Download file 15 kB
-
https://huggingface.co/TTS-AGI/laionbox-ablation-checkpoints/resolve/main/code/scripts/run_ablation_overnight.py
- Command line
-
hf download hf://TTS-AGI/laionbox-ablation-checkpoints/code/scripts/run_ablation_overnight.py
-
curl -L -o run_ablation_overnight.py https://huggingface.co/TTS-AGI/laionbox-ablation-checkpoints/resolve/main/code/scripts/run_ablation_overnight.py
15 kB
| #!/usr/bin/env python3 | |
| """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})") | |
| # Find closest saved checkpoint (save_every=10) | |
| def find_ckpt(step): | |
| if step is None: | |
| return None, None | |
| # Try exact step first, then nearby | |
| for offset in [0, -1, 1, -2, 2, -3, 3, -4, 4, -5, 5]: | |
| candidate_step = (step + offset * 10) if offset != 0 else step | |
| # Try step checkpoint | |
| 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 | |
| # Also try without zero padding | |
| 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 | |
| # Try epoch checkpoints | |
| 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 | |
| # Round step to nearest save_every=10 | |
| 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) | |
| # Ensure accelerate from ml-general is used | |
| 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}") | |
| # Wait for completion | |
| t0 = time.time() | |
| while True: | |
| ret = proc.poll() | |
| if ret is not None: | |
| break | |
| elapsed = time.time() - t0 | |
| # Print progress every 5 minutes | |
| if int(elapsed) % 300 == 0 and elapsed > 10: | |
| # Read last line of status.json if it exists | |
| 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("") | |
| # Phase 1: Training | |
| results = {} | |
| for abl in ABLATIONS: | |
| # Check if this ablation already completed (metrics.jsonl has >= 20 entries) | |
| 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, # (path, step, loss) | |
| "nat": nat_info, # (path, step, nat_score) | |
| } | |
| # Upload to HF | |
| 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("") | |
| # Phase 2: Build models dict for evaluation | |
| 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']}") | |
| # Write models JSON | |
| models_json = os.path.join(VAP_DIR, "ablation_eval_models.json") | |
| write_eval_models_json(models, models_json) | |
| # Phase 3: Evaluation | |
| eval_output = os.path.join(VAP_DIR, "ablation_eval") | |
| success = run_eval(models_json, eval_output) | |
| # Phase 4: Summary | |
| 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") | |
| # Print training summary | |
| 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() | |