mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-07-14 07:57:21 +02:00
04b8ec60cb
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>
203 lines
7.4 KiB
Python
203 lines
7.4 KiB
Python
"""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": "7B–20B 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.",
|
||
}
|