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

310 lines
10 KiB
Python

"""Community contribution system for crowdsourced paper data.
Enables users to contribute anonymized experiment results to the shared
paper dataset. Unlike telemetry (which is fire-and-forget to a remote
endpoint), contributions are saved as local JSON files that can be
submitted via pull request to the community results repository.
Usage:
from obliteratus.community import save_contribution
# After running a pipeline:
path = save_contribution(
pipeline,
model_name="meta-llama/Llama-2-7b-chat-hf", # public model ID
notes="Ran on A100 with default prompts",
)
# Generates: community_results/llama2-7b_advanced_20260227_143052.json
"""
from __future__ import annotations
import hashlib
import json
import logging
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from obliteratus.telemetry import (
_direction_stats,
_extract_excise_details,
_extract_prompt_counts,
_extract_stage_durations,
_get_peak_vram,
_safe_float,
build_report,
)
logger = logging.getLogger(__name__)
# Schema version for community contributions (extends telemetry schema v2)
CONTRIBUTION_SCHEMA_VERSION = 1
# Default output directory for contributions
DEFAULT_CONTRIB_DIR = "community_results"
def _model_short_name(model_name: str) -> str:
"""Extract a filesystem-safe short name from a HuggingFace model ID."""
# "meta-llama/Llama-2-7b-chat-hf" -> "llama-2-7b-chat-hf"
name = model_name.split("/")[-1].lower()
name = re.sub(r"[^a-z0-9\-]", "-", name)
name = re.sub(r"-+", "-", name).strip("-")
return name[:60] # cap length
def _config_fingerprint(config: dict[str, Any]) -> str:
"""Deterministic short hash of the method configuration."""
canonical = json.dumps(config, sort_keys=True, default=str)
return hashlib.sha256(canonical.encode()).hexdigest()[:8]
def save_contribution(
pipeline,
*,
model_name: str,
notes: str = "",
output_dir: str | Path = DEFAULT_CONTRIB_DIR,
informed_report=None,
) -> Path:
"""Save a contribution record from a completed pipeline run.
Unlike telemetry, this:
- Includes the public model name (for aggregation by model)
- Saves locally (not sent remotely)
- Uses a human-readable filename
- Includes a config fingerprint for deduplication
- Is always explicit (no silent opt-in)
Args:
pipeline: A completed AbliterationPipeline instance.
model_name: HuggingFace model ID (e.g., "meta-llama/Llama-2-7b-chat-hf").
notes: Optional free-text notes about the run.
output_dir: Directory to save contribution files.
informed_report: Optional InformedPipelineReport for informed pipeline runs.
Returns:
Path to the saved contribution JSON file.
"""
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Build the base telemetry report (reuse existing schema)
summary = pipeline.handle.summary()
config_keys = [
"n_directions", "norm_preserve", "regularization",
"refinement_passes", "project_biases", "use_chat_template",
"use_whitened_svd", "true_iterative_refinement",
"use_jailbreak_contrast", "layer_adaptive_strength",
"attention_head_surgery", "safety_neuron_masking",
"per_expert_directions", "use_sae_features", "invert_refusal",
"project_embeddings", "embed_regularization",
"activation_steering", "steering_strength",
"expert_transplant", "transplant_blend",
"reflection_strength",
]
method_config = {}
for key in config_keys:
val = getattr(pipeline, key, None)
if val is not None:
method_config[key] = val
# Extract analysis insights if informed report is available
analysis_insights = None
informed_extras = None
if informed_report is not None:
try:
from obliteratus.telemetry import _extract_analysis_insights
analysis_insights = _extract_analysis_insights(informed_report)
informed_extras = {}
if hasattr(informed_report, "ouroboros_passes"):
informed_extras["ouroboros_passes"] = informed_report.ouroboros_passes
if hasattr(informed_report, "final_refusal_rate"):
informed_extras["final_refusal_rate"] = _safe_float(
informed_report.final_refusal_rate
)
except Exception:
logger.debug("Failed to extract analysis insights from informed report", exc_info=True)
base_report = build_report(
architecture=summary.get("architecture", "unknown"),
num_layers=summary.get("num_layers", 0),
num_heads=summary.get("num_heads", 0),
hidden_size=summary.get("hidden_size", 0),
total_params=summary.get("total_params", 0),
method=pipeline.method,
method_config=method_config,
quality_metrics=pipeline._quality_metrics,
stage_durations=_extract_stage_durations(pipeline),
strong_layers=pipeline._strong_layers,
direction_stats=_direction_stats(pipeline),
excise_details=_extract_excise_details(pipeline),
prompt_counts=_extract_prompt_counts(pipeline),
gpu_memory=_get_peak_vram(),
analysis_insights=analysis_insights,
informed_extras=informed_extras,
)
# Wrap in community contribution envelope
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
contribution = {
"contribution_schema_version": CONTRIBUTION_SCHEMA_VERSION,
"timestamp": timestamp,
"model_name": model_name,
"config_fingerprint": _config_fingerprint(method_config),
"notes": notes,
"telemetry": base_report,
}
# Generate filename
short_name = _model_short_name(model_name)
method = pipeline.method
ts_short = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
filename = f"{short_name}_{method}_{ts_short}.json"
filepath = output_dir / filename
filepath.write_text(json.dumps(contribution, indent=2, default=str))
logger.info("Community contribution saved: %s", filepath)
return filepath
def load_contributions(
contrib_dir: str | Path = DEFAULT_CONTRIB_DIR,
) -> list[dict[str, Any]]:
"""Load all contribution records from a directory.
Args:
contrib_dir: Directory containing contribution JSON files.
Returns:
List of parsed contribution records, sorted by timestamp.
"""
contrib_dir = Path(contrib_dir)
if not contrib_dir.exists():
return []
records = []
for path in sorted(contrib_dir.glob("*.json")):
try:
data = json.loads(path.read_text())
if "contribution_schema_version" in data:
data["_source_file"] = str(path)
records.append(data)
except (json.JSONDecodeError, OSError) as e:
logger.warning("Skipping invalid contribution file %s: %s", path, e)
records.sort(key=lambda r: r.get("timestamp", ""))
return records
def aggregate_results(
records: list[dict[str, Any]],
) -> dict[str, dict[str, Any]]:
"""Aggregate contribution records into per-model, per-method summaries.
Groups results by (model_name, method) and computes summary statistics
for key metrics (refusal_rate, perplexity, coherence).
Returns:
Nested dict: {model_name: {method: {metric: {mean, std, n, values}}}}
"""
import statistics
groups: dict[tuple[str, str], list[dict]] = {}
for record in records:
model = record.get("model_name", "unknown")
telemetry = record.get("telemetry", {})
method = telemetry.get("method", "unknown")
metrics = telemetry.get("quality_metrics", {})
key = (model, method)
if key not in groups:
groups[key] = []
groups[key].append(metrics)
results: dict[str, dict[str, Any]] = {}
for (model, method), metric_list in groups.items():
if model not in results:
results[model] = {}
summary: dict[str, Any] = {"n_runs": len(metric_list)}
for metric_name in ["refusal_rate", "perplexity", "coherence"]:
values = [
m[metric_name]
for m in metric_list
if metric_name in m and m[metric_name] is not None
]
if values:
summary[metric_name] = {
"mean": round(statistics.mean(values), 4),
"std": round(statistics.stdev(values), 4) if len(values) > 1 else 0.0,
"n": len(values),
"min": round(min(values), 4),
"max": round(max(values), 4),
}
results[model][method] = summary
return results
def generate_latex_table(
aggregated: dict[str, dict[str, Any]],
methods: list[str] | None = None,
metric: str = "refusal_rate",
) -> str:
"""Generate a LaTeX table from aggregated community results.
Args:
aggregated: Output of aggregate_results().
methods: Methods to include (default: all found).
metric: Which metric to display (default: refusal_rate).
Returns:
LaTeX table source string.
"""
if methods is None:
all_methods: set[str] = set()
for model_data in aggregated.values():
all_methods.update(model_data.keys())
methods = sorted(all_methods)
# Build header
method_cols = " & ".join(f"\\textbf{{{m}}}" for m in methods)
header = f"\\textbf{{Model}} & {method_cols} \\\\"
lines = [
"\\begin{tabular}{@{}l" + "c" * len(methods) + "@{}}",
"\\toprule",
header,
"\\midrule",
]
for model in sorted(aggregated.keys()):
model_data = aggregated[model]
short = model.split("/")[-1] if "/" in model else model
cells = []
for method in methods:
if method in model_data and metric in model_data[method]:
stats = model_data[method][metric]
mean = stats["mean"]
n = stats["n"]
if stats["std"] > 0 and n > 1:
cells.append(f"{mean:.1f}$\\pm${stats['std']:.1f} ({n})")
else:
cells.append(f"{mean:.1f} ({n})")
else:
cells.append("---")
row = f"{short} & " + " & ".join(cells) + " \\\\"
lines.append(row)
lines.extend(["\\bottomrule", "\\end{tabular}"])
return "\n".join(lines)