Files
OBLITERATUS/obliteratus/sweep.py
T
2026-03-04 12:38:18 -08:00

149 lines
4.5 KiB
Python

"""Hyperparameter sweep runner for ablation studies.
Systematically varies abliteration hyperparameters to answer:
- Does n_directions=4 actually outperform n_directions=1?
- Does regularization help or hurt?
- How many refinement passes are needed before diminishing returns?
- Is whitened SVD actually better than standard SVD?
Usage:
from obliteratus.sweep import run_sweep, SweepConfig
config = SweepConfig(
model_name="meta-llama/Llama-3.1-8B-Instruct",
sweep_params={
"n_directions": [1, 2, 4, 8],
"regularization": [0.0, 0.1, 0.3],
},
# Fixed params for all runs:
fixed_params={"norm_preserve": True, "method": "advanced"},
)
results = run_sweep(config)
results.to_csv("sweep_results.csv")
"""
from __future__ import annotations
import itertools
import json
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
logger = logging.getLogger(__name__)
@dataclass
class SweepConfig:
"""Configuration for a hyperparameter sweep."""
model_name: str
sweep_params: dict[str, list[Any]]
fixed_params: dict[str, Any] = field(default_factory=dict)
output_dir: str = "sweep_results"
seed: int = 42
n_seeds: int = 1 # run each config with multiple seeds for variance
@dataclass
class SweepResult:
"""Results from a single sweep configuration."""
params: dict[str, Any]
seed: int
quality_metrics: dict[str, Any]
stage_durations: dict[str, float]
strong_layers: list[int]
error: str | None = None
def _param_grid(sweep_params: dict[str, list[Any]]) -> list[dict[str, Any]]:
"""Generate all combinations of sweep parameters."""
keys = sorted(sweep_params.keys())
values = [sweep_params[k] for k in keys]
configs = []
for combo in itertools.product(*values):
configs.append(dict(zip(keys, combo)))
return configs
def run_sweep(config: SweepConfig) -> list[SweepResult]:
"""Run a hyperparameter sweep over abliteration configurations.
For each combination of sweep_params (crossed with n_seeds random seeds),
runs the full abliteration pipeline and records quality metrics.
Args:
config: SweepConfig specifying the sweep grid.
Returns:
List of SweepResult, one per (param_config, seed) pair.
"""
from obliteratus.abliterate import AbliterationPipeline
grid = _param_grid(config.sweep_params)
total_runs = len(grid) * config.n_seeds
logger.info("Sweep: %d configs x %d seeds = %d total runs", len(grid), config.n_seeds, total_runs)
output_dir = Path(config.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
results: list[SweepResult] = []
for run_idx, (params, seed_offset) in enumerate(
itertools.product(grid, range(config.n_seeds))
):
seed = config.seed + seed_offset
run_params = {**config.fixed_params, **params}
logger.info(
"[%d/%d] params=%s seed=%d",
run_idx + 1, total_runs, params, seed,
)
try:
pipeline = AbliterationPipeline(
model_name=config.model_name,
output_dir=str(output_dir / f"run_{run_idx:03d}"),
seed=seed,
**run_params,
)
pipeline.run()
result = SweepResult(
params=params,
seed=seed,
quality_metrics=dict(pipeline._quality_metrics),
stage_durations=dict(pipeline._stage_durations),
strong_layers=list(pipeline._strong_layers),
)
except Exception as e:
logger.error("Run %d failed: %s", run_idx, e)
result = SweepResult(
params=params,
seed=seed,
quality_metrics={},
stage_durations={},
strong_layers=[],
error=str(e),
)
results.append(result)
# Save incremental results
_save_results(results, output_dir / "sweep_results.json")
return results
def _save_results(results: list[SweepResult], path: Path) -> None:
"""Save sweep results to JSON."""
data = []
for r in results:
data.append({
"params": r.params,
"seed": r.seed,
"quality_metrics": r.quality_metrics,
"stage_durations": r.stage_durations,
"strong_layers": r.strong_layers,
"error": r.error,
})
path.write_text(json.dumps(data, indent=2, default=str))