Files
faber 04b8ec60cb Add ASPA framework, AutoObliterator, Watchtower, expanded eval corpus
New core modules:
- auto_obliterate.py: Automated multi-iteration obliteration pipeline
- watchtower.py: HF Hub model discovery and tracking
- ui_watchtower.py: Gradio tabs for Watchtower (ready for app.py wiring)
- hard_negative.py: Residue mining from refusal audits
- model_profile.py: Parameter profiling from safetensors/config
- bestiary_sync.py: Sync models from PlinyOS BESTIARY registry
- models_client.py: Lightweight HF model list client

Framework enhancements:
- abliterate.py: ASPA source-tethering, step gradient blending, hard-negative residue support
- cli.py: self-improve command, model profiling, hard-negative flags
- prompts.py: Expanded 842-prompt refusal eval corpus across 10 categories
- __init__.py: New exports (Watchtower, AutoObliterator)

Reference implementations (14 scripts):
- ASPA sweep, gradient search, coherence eval, MMLU benchmarks
- Pareto controller, refusal sniper, stock comparisons

Documentation:
- README: Research framing, responsible use section, comprehensive disclaimer
- docs/beyond_sota_roadmap.md, docs/recursive_self_improvement.md

Tests: 4 new test files (354 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 03:54:38 -04:00

203 lines
7.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Lightweight model parameter profiling for OBLITERATUS planning/eval.
Avoids loading full weights. For local safetensors artifacts it can count
parameters exactly from tensor metadata; for Hub/config-only targets it falls
back to architecture-aware config estimates, including composite Qwen configs
whose text dimensions live under ``text_config``.
"""
from __future__ import annotations
import json
import math
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
@dataclass(frozen=True)
class ModelProfile:
model: str
source: str
total_params: int | None
total_params_b: float | None
active_params_b: float | None
num_layers: int | None
hidden_size: int | None
intermediate_size: int | None
vocab_size: int | None
model_type: str | None
dtype: str | None = None
def to_json(self) -> dict[str, Any]:
return asdict(self)
def _cfg_get(cfg: Any, name: str, default: Any = None) -> Any:
if isinstance(cfg, dict):
return cfg.get(name, default)
return getattr(cfg, name, default)
def _text_cfg(cfg: Any) -> Any:
return _cfg_get(cfg, "text_config") or {}
def _dim(cfg: Any, name: str, default: Any = 0) -> Any:
val = _cfg_get(cfg, name, None)
if val not in (None, 0):
return val
return _cfg_get(_text_cfg(cfg), name, default)
def _local_config(model: str | Path) -> dict[str, Any] | None:
path = Path(model).expanduser()
cfg_path = path / "config.json" if path.is_dir() else path
if cfg_path.exists() and cfg_path.name == "config.json":
return json.loads(cfg_path.read_text())
return None
def _count_safetensors_params(model_dir: Path) -> int | None:
try:
from safetensors import safe_open
except Exception:
return None
if not model_dir.is_dir():
return None
files = sorted(model_dir.glob("*.safetensors"))
if not files:
return None
seen: set[str] = set()
total = 0
for file in files:
with safe_open(file, framework="pt", device="cpu") as sf:
for key in sf.keys():
if key in seen:
continue
seen.add(key)
try:
shape = sf.get_slice(key).get_shape()
except Exception:
shape = sf.get_tensor(key).shape
total += math.prod(int(x) for x in shape)
return total
def estimate_total_params(config: Any) -> int | None:
"""Estimate total params from config, falling through to text_config.
Prefer exact-ish explicit attributes when present; otherwise use an
architecture-aware transformer estimate. Local artifacts should use
safetensors metadata for exact counts instead.
"""
for attr in ("num_parameters", "n_params", "total_params"):
val = _dim(config, attr, None)
if val and float(val) > 1000:
return int(val)
h = int(_dim(config, "hidden_size", 0) or 0)
layers = int(_dim(config, "num_hidden_layers", 0) or 0)
vocab = int(_dim(config, "vocab_size", 0) or 0)
inter = int(_dim(config, "intermediate_size", h * 4) or h * 4)
if h <= 0 or layers <= 0:
return None
n_heads = int(_dim(config, "num_attention_heads", 0) or max(1, h // 128))
head_dim = int(_dim(config, "head_dim", 0) or max(1, h // n_heads))
kv_heads = int(_dim(config, "num_key_value_heads", 0) or n_heads)
n_experts = int(_dim(config, "num_local_experts", 0) or _dim(config, "num_experts", 1) or 1)
moe_inter = int(_dim(config, "moe_intermediate_size", 0) or inter)
# Attention projections: q, k, v, o. Handles GQA/MQA by using kv_heads.
attn = h * (n_heads * head_dim) + 2 * h * (kv_heads * head_dim) + (n_heads * head_dim) * h
ffn = 3 * h * moe_inter * n_experts
if n_experts > 1 and _dim(config, "moe_intermediate_size", None) is not None:
ffn += 3 * h * inter
ffn += h * n_experts # router-ish
norms = 4 * h
embeddings = 2 * vocab * h
return int(layers * (attn + ffn + norms) + embeddings)
def estimate_active_params_b(config: Any, total_params_b: float) -> float:
n_experts = int(_dim(config, "num_local_experts", 0) or _dim(config, "num_experts", 1) or 1)
if n_experts <= 1:
return total_params_b
top_k = int(_dim(config, "num_experts_per_tok", 0) or _dim(config, "top_k", 2) or 2)
h = int(_dim(config, "hidden_size", 0) or 0)
layers = int(_dim(config, "num_hidden_layers", 0) or 0)
inter = int(_dim(config, "intermediate_size", h * 4) or h * 4)
moe_inter = int(_dim(config, "moe_intermediate_size", inter) or inter)
if h <= 0 or layers <= 0:
return total_params_b
ffn_per_expert = 3 * h * moe_inter
ffn_all = ffn_per_expert * n_experts * layers
ffn_active = ffn_per_expert * min(top_k, n_experts) * layers
non_ffn = total_params_b * 1e9 - ffn_all
return max((non_ffn + ffn_active) / 1e9, 0.1)
def profile_model(model: str, *, dtype: str | None = None, trust_remote_code: bool = True) -> ModelProfile:
path = Path(model).expanduser()
config: Any | None = _local_config(path)
source = "local_config" if config is not None else "hf_config"
exact_params = _count_safetensors_params(path) if path.is_dir() else None
if config is None:
from transformers import AutoConfig
config = AutoConfig.from_pretrained(model, trust_remote_code=trust_remote_code)
total_params = exact_params or estimate_total_params(config)
total_b = (total_params / 1e9) if total_params else None
active_b = estimate_active_params_b(config, total_b) if total_b else None
if exact_params is not None:
source = "local_safetensors"
return ModelProfile(
model=str(path) if path.exists() else model,
source=source,
total_params=total_params,
total_params_b=round(total_b, 6) if total_b is not None else None,
active_params_b=round(active_b, 6) if active_b is not None else None,
num_layers=int(_dim(config, "num_hidden_layers", 0) or 0) or None,
hidden_size=int(_dim(config, "hidden_size", 0) or 0) or None,
intermediate_size=int(_dim(config, "intermediate_size", 0) or 0) or None,
vocab_size=int(_dim(config, "vocab_size", 0) or 0) or None,
model_type=str(_dim(config, "model_type", "") or "") or None,
dtype=dtype or str(_dim(config, "dtype", "") or _dim(config, "torch_dtype", "") or "") or None,
)
def default_self_improve_params(profile: ModelProfile) -> dict[str, Any]:
"""Conservative size-aware defaults for recursive residue runs."""
p = profile.total_params_b or 0.0
if p >= 20:
return {
"n_directions": 3,
"regularization": 0.30,
"refinement_passes": 1,
"residue_weight": 3,
"verify_sample_size": 30,
"note": "20B+ model: conservative residue weight and one refinement pass to reduce collapse risk.",
}
if p >= 7:
return {
"n_directions": 3,
"regularization": 0.28,
"refinement_passes": 1,
"residue_weight": 5,
"verify_sample_size": 40,
"note": "7B20B model: moderate residue pressure.",
}
return {
"n_directions": 2,
"regularization": 0.25,
"refinement_passes": 1,
"residue_weight": 5,
"verify_sample_size": 50,
"note": "Small model: lower direction count, larger verify sample.",
}