Klaus Clawd
Release v0.2.1: recover attack strength, cross-arch judges, uncapped frontier eval
255b4a8 | """Uniform, differentiable encoder interface for the ensemble PGD attack. | |
| Every encoder exposes: | |
| - image_feat(x): [0,1] (B,3,H,W) -> L2-normalized image embedding (differentiable) | |
| - text_feat(list[str]) -> L2-normalized text embeddings (contrastive encoders only) | |
| - kind: "contrastive" (has text tower -> targeted decoy loss) or "feature" | |
| (raw patch features -> untargeted feature-displacement loss) | |
| The contrastive backbone = open_clip encoders (native text direction, cheap, | |
| architecturally diverse incl. a ConvNeXt CNN). HF VLM vision towers (MoonViT, | |
| InternViT, MiniMax-M3) are added as "feature" encoders via ensemble/towers/. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from dataclasses import dataclass | |
| import torch | |
| import torch.nn.functional as F | |
| _DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| _TOWERS_DIR = os.path.join(os.path.dirname(__file__), "towers") | |
| # name : (open_clip_arch, pretrained_tag). Grouped for architectural diversity. | |
| OPENCLIP_SPECS: dict[str, tuple[str, str]] = { | |
| # OpenAI / LAION classic ViT | |
| "clip-L14-openai": ("ViT-L-14", "openai"), | |
| "clip-H14-laion2b": ("ViT-H-14", "laion2b_s32b_b79k"), | |
| "clip-bigG14-laion2b": ("ViT-bigG-14", "laion2b_s39b_b160k"), | |
| # EVA (different pretraining/arch) | |
| "eva02-L14-336": ("EVA02-L-14-336", "merged2b_s6b_b61k"), | |
| "eva02-E14": ("EVA02-E-14", "laion2b_s4b_b115k"), | |
| # SigLIP / SigLIP2 (sigmoid loss, SO400m shape) | |
| "siglip2-L16-256": ("ViT-L-16-SigLIP2-256", "webli"), | |
| "siglip2-so400m-378": ("ViT-SO400M-14-SigLIP2-378", "webli"), | |
| "siglip-so400m-384": ("ViT-SO400M-14-SigLIP-384", "webli"), | |
| # MetaCLIP + DFN (different data curation) | |
| "metaclip-L14": ("ViT-L-14", "metaclip_fullcc"), | |
| # DFN weights were trained with QuickGELU — use the canonical -quickgelu arch so the | |
| # activation matches (plain arch silently loads GELU and corrupts the features). | |
| "dfn-H14-378": ("ViT-H-14-378-quickgelu", "dfn5b"), | |
| "dfn-L14": ("ViT-L-14-quickgelu", "dfn2b"), | |
| # ConvNeXt — a CNN, not a ViT (true architectural diversity) | |
| "convnext-xxl-laion2b": ("convnext_xxlarge", "laion2b_s34b_b82k_augreg"), | |
| # v0.2.1 Tier-1 contrastive giants (differentiability-probed on klaus-1) | |
| "siglip2-giant-384": ("ViT-gopt-16-SigLIP2-384", "webli"), # largest public SigLIP2 | |
| "metaclip2-H-worldwide-378": ("ViT-H-14-worldwide-378", "metaclip2_worldwide"), # newest Meta, multiling | |
| } | |
| # Default train/held-out split so transfer is measured on unseen architectures. | |
| DEFAULT_TRAIN = [ | |
| "clip-L14-openai", "clip-H14-laion2b", "clip-bigG14-laion2b", | |
| "eva02-L14-336", "siglip2-L16-256", "siglip-so400m-384", | |
| "metaclip-L14", "convnext-xxl-laion2b", | |
| ] | |
| DEFAULT_HELDOUT = ["dfn-H14-378", "dfn-L14", "eva02-E14", "siglip2-so400m-378"] | |
| # Feature towers = actual vision encoders inside recent open VLMs, loaded via timm | |
| # (clean forward_features, differentiable, no flash_attn / trust_remote_code pain). | |
| # No text tower -> "feature" kind (untargeted repel loss only). Chosen for being | |
| # real VLM backbones AND architecturally novel vs the CLIP-family train set. | |
| TIMM_FEATURE_SPECS: dict[str, str] = { | |
| "aimv2-huge-336": "aimv2_huge_patch14_336.apple_pt", # AIMv2 (GLM-4V family init) | |
| "dinov2-L-reg": "vit_large_patch14_reg4_dinov2.lvd142m", # DINOv2 self-supervised | |
| } | |
| # Ablation train set: the 8 contrastive encoders + 2 real VLM feature towers. | |
| ABLATION_TRAIN = DEFAULT_TRAIN + ["aimv2-huge-336", "dinov2-L-reg"] | |
| # Modern-LLM vision towers, loaded via hand-written differentiable loaders under | |
| # ensemble/towers/ (verified grad-flow on the Sparks). "feature" kind -> untargeted | |
| # repel loss only. These are the actual encoders inside recent open VLMs, so they're | |
| # the closest open proxies for frontier vision behavior. | |
| # name : (loader_module, load_fn, feat_fn) | |
| CUSTOM_SPECS: dict[str, tuple[str, str, str]] = { | |
| "moonvit-so400m": ("moonvit_loader", "load_moonvit", "image_feat_moonvit"), # Kimi K2.7 | |
| "internvit-300m-v2_5": ("internvit_loader", "load_internvit", "image_feat_internvit"), # InternVL3.5 | |
| "minimax-m3-vision": ("minimax_loader", "load_minimax", "image_feat_minimax"), # MiniMax-M3 | |
| # v0.2.1 Tier-1 held-out judges (never attacked; cross-architecture on purpose): | |
| # agglomerative distillation of CLIP+SigLIP2+DINOv2+SAM ... | |
| "c-radio-v3-h": ("cradio_loader", "load_cradio", "image_feat_cradio"), | |
| # ... and a real frontier VLM captioner tower (Qwen3-VL-8B vision). | |
| "qwen3vl-8b-vision": ("qwen3vl_loader", "load_qwen3vl", "image_feat_qwen3vl"), | |
| } | |
| # Full train set: ablation set (contrastive + AIMv2/DINOv2) + the 3 modern-LLM towers. | |
| # This is the complete ensemble used for the v0.1 results. | |
| FULL_TRAIN = ABLATION_TRAIN + list(CUSTOM_SPECS) | |
| # --- v0.2 split (M1): the held-out set must contain architecture FAMILIES the train | |
| # set never sees, so "transfer" measures cross-architecture generalization rather than | |
| # CLIP-family self-similarity. Feature/VLM towers now sit in held-out too; with M4 | |
| # decoy-image centroids we can score a real flip on them (adv closer to the decoy | |
| # centroid than the truth centroid), not just a contrastive text margin. | |
| V02_TRAIN = [ | |
| # contrastive core (8) | |
| "clip-L14-openai", "clip-H14-laion2b", "clip-bigG14-laion2b", | |
| "eva02-L14-336", "siglip2-L16-256", "siglip-so400m-384", | |
| "metaclip-L14", "convnext-xxl-laion2b", | |
| # feature / VLM towers kept in train | |
| "aimv2-huge-336", # SSL feature family (train) | |
| "moonvit-so400m", # VLM tower (Kimi) | |
| "minimax-m3-vision", # VLM tower (MiniMax-M3) | |
| ] | |
| V02_HELDOUT = [ | |
| "dfn-H14-378", "dfn-L14", "siglip2-so400m-378", # CLIP-family contrastive anchors | |
| "dinov2-L-reg", # SSL feature family NOT in train (cross-arch) | |
| "internvit-300m-v2_5", # VLM tower NOT in train (cross-arch) | |
| ] | |
| # --- v0.2.1 split (ADOPTED): the v0.2 comparison showed moving DINOv2+InternViT OUT of the | |
| # attack (to serve as held-out) COST attack strength vs v0.1 (esp. on gpt-5.5, 20.7%->6.9%). | |
| # Fix: put the FULL v0.1 ensemble back in the ATTACK, and use SEPARATE never-trained encoders | |
| # as cross-arch JUDGES so measurement no longer cannibalizes the attack. This recovers gpt-5.5 | |
| # to 20.7% and lifts pooled frontier flip to 38.8% (best of v0.1/v0.2/v0.2.1). See | |
| # research/v0_2_1_findings.md. | |
| # | |
| # The two Tier-1 contrastive giants (SigLIP2-giant, MetaCLIP2-H) were ablation-gated and | |
| # REJECTED — adding them to the attack hurt frontier (-3.0pp BOTH) and train margin | |
| # (-3.1pp, CI excludes 0) by diluting the per-step subset with near-CLIP clones. The adopted | |
| # attack therefore does NOT include them. | |
| V021_ATTACK = [ | |
| # contrastive core (8) | |
| "clip-L14-openai", "clip-H14-laion2b", "clip-bigG14-laion2b", | |
| "eva02-L14-336", "siglip2-L16-256", "siglip-so400m-384", | |
| "metaclip-L14", "convnext-xxl-laion2b", | |
| # feature / VLM towers (all back in the attack — recover v0.1 strength) | |
| "aimv2-huge-336", "dinov2-L-reg", | |
| "moonvit-so400m", "internvit-300m-v2_5", "minimax-m3-vision", | |
| ] | |
| # Opt-in ablation arm: attack + the two REJECTED Tier-1 giants (kept only to reproduce the gate). | |
| V021_ATTACK_GIANTS = V021_ATTACK + ["siglip2-giant-384", "metaclip2-H-worldwide-378"] | |
| # Back-compat alias: the gate baseline == the adopted attack. | |
| V021_ATTACK_BASE = V021_ATTACK | |
| # Held-out JUDGES: never in any attack set. Cross-architecture on purpose. | |
| V021_HELDOUT = [ | |
| "dfn-H14-378", "dfn-L14", # CLIP-family contrastive anchors | |
| "c-radio-v3-h", # agglomerative (CLIP+SigLIP2+DINOv2+SAM) — max-diversity judge | |
| "qwen3vl-8b-vision", # real frontier VLM captioner tower — the closest open proxy | |
| ] | |
| # Architectural family per encoder — used by stratified subset sampling so a | |
| # single PGD step can't be dominated by near-clones (e.g. all OpenAI-style ViTs). | |
| # Group by backbone architecture, not by pretraining data. | |
| FAMILY: dict[str, str] = { | |
| "clip-L14-openai": "clip_vit", "clip-H14-laion2b": "clip_vit", | |
| "clip-bigG14-laion2b": "clip_vit", "metaclip-L14": "clip_vit", | |
| "dfn-H14-378": "clip_vit", "dfn-L14": "clip_vit", | |
| "eva02-L14-336": "eva", "eva02-E14": "eva", | |
| "siglip2-L16-256": "siglip", "siglip2-so400m-378": "siglip", | |
| "siglip-so400m-384": "siglip", | |
| "convnext-xxl-laion2b": "convnext", | |
| "aimv2-huge-336": "aimv2", "dinov2-L-reg": "dinov2", | |
| "moonvit-so400m": "moonvit", "internvit-300m-v2_5": "internvit", | |
| "minimax-m3-vision": "minimax", | |
| # v0.2.1 Tier-1 | |
| "siglip2-giant-384": "siglip", "metaclip2-H-worldwide-378": "metaclip2", | |
| "c-radio-v3-h": "radio", "qwen3vl-8b-vision": "qwen3vl", | |
| } | |
| def family_of(name: str) -> str: | |
| return FAMILY.get(name, name) | |
| class Encoder: | |
| name: str | |
| kind: str # "contrastive" | "feature" | |
| res: int | |
| model: object | |
| tokenizer: object | |
| mean: torch.Tensor | |
| std: torch.Tensor | |
| dtype: torch.dtype | |
| backend: str = "openclip" # "openclip" | "timm" | "custom" | |
| family: str = "" | |
| feat_fn: object = None # custom towers: feat_fn(model, x[0,1]) -> (1, D) | |
| def _prep(self, x: torch.Tensor) -> torch.Tensor: | |
| x = F.interpolate(x, size=(self.res, self.res), mode="bicubic", | |
| align_corners=False, antialias=True).clamp(0, 1) | |
| x = (x - self.mean) / self.std | |
| return x.to(self.dtype) | |
| def image_feat(self, x: torch.Tensor) -> torch.Tensor: | |
| if self.backend == "custom": | |
| f = self.feat_fn(self.model, x) # does its own resize/normalize | |
| elif self.backend == "timm": | |
| f = self.model(self._prep(x)) # num_classes=0 -> pooled features | |
| if f.dim() == 3: # (B, N, D) if pooling disabled | |
| f = f.mean(dim=1) | |
| else: | |
| f = self.model.encode_image(self._prep(x)) | |
| return f / f.norm(dim=-1, keepdim=True) | |
| def text_feat(self, texts: list[str]) -> torch.Tensor: | |
| assert self.kind == "contrastive" | |
| tok = self.tokenizer(texts).to(_DEVICE) | |
| f = self.model.encode_text(tok) | |
| return f / f.norm(dim=-1, keepdim=True) | |
| def _mean_std_from_preprocess(preprocess) -> tuple[list, list]: | |
| for t in getattr(preprocess, "transforms", []): | |
| if t.__class__.__name__ == "Normalize": | |
| return list(t.mean), list(t.std) | |
| # OpenAI CLIP defaults | |
| return ([0.48145466, 0.4578275, 0.40821073], | |
| [0.26862954, 0.26130258, 0.27577711]) | |
| def load_openclip(name: str, dtype=torch.float16) -> Encoder: | |
| import open_clip | |
| arch, tag = OPENCLIP_SPECS[name] | |
| # OpenAI + MetaCLIP + DFN weights were trained with QuickGELU; force it to match | |
| # (DFN also uses a -quickgelu arch name, but keep the flag for belt-and-suspenders). | |
| force_qgelu = tag in ("openai", "metaclip_400m", "metaclip_fullcc", "dfn2b", "dfn5b") | |
| model, _, preprocess = open_clip.create_model_and_transforms( | |
| arch, pretrained=tag, force_quick_gelu=force_qgelu) | |
| model = model.to(_DEVICE, dtype=dtype).eval() | |
| for p in model.parameters(): | |
| p.requires_grad_(False) | |
| tok = open_clip.get_tokenizer(arch) | |
| mean, std = _mean_std_from_preprocess(preprocess) | |
| res = getattr(model.visual, "image_size", 224) | |
| res = res[0] if isinstance(res, (tuple, list)) else int(res) | |
| m = torch.tensor(mean, device=_DEVICE).view(1, 3, 1, 1) | |
| s = torch.tensor(std, device=_DEVICE).view(1, 3, 1, 1) | |
| return Encoder(name, "contrastive", res, model, tok, m, s, dtype, | |
| family=family_of(name)) | |
| def load_timm_feature(name: str, dtype=torch.float16) -> Encoder: | |
| import timm | |
| model = timm.create_model(TIMM_FEATURE_SPECS[name], pretrained=True, num_classes=0) | |
| model = model.to(_DEVICE, dtype=dtype).eval() | |
| for p in model.parameters(): | |
| p.requires_grad_(False) | |
| cfg = timm.data.resolve_model_data_config(model) | |
| res = cfg["input_size"][-1] | |
| m = torch.tensor(cfg["mean"], device=_DEVICE).view(1, 3, 1, 1) | |
| s = torch.tensor(cfg["std"], device=_DEVICE).view(1, 3, 1, 1) | |
| return Encoder(name, "feature", res, model, None, m, s, dtype, | |
| backend="timm", family=family_of(name)) | |
| def load_custom_tower(name: str, dtype=torch.bfloat16) -> Encoder: | |
| """Load a modern-LLM vision tower via its hand-written loader in ensemble/towers/.""" | |
| import importlib | |
| if _TOWERS_DIR not in sys.path: | |
| sys.path.insert(0, _TOWERS_DIR) | |
| mod_name, load_fn, feat_fn = CUSTOM_SPECS[name] | |
| mod = importlib.import_module(mod_name) | |
| model = getattr(mod, load_fn)(dtype=dtype) | |
| fn = getattr(mod, feat_fn) | |
| dummy = torch.zeros(1, device=_DEVICE) | |
| return Encoder(name, "feature", 0, model, None, dummy, dummy, dtype, | |
| backend="custom", family=family_of(name), feat_fn=fn) | |
| def load_encoder(name: str, dtype=torch.float16) -> Encoder: | |
| if name in OPENCLIP_SPECS: | |
| return load_openclip(name, dtype=dtype) | |
| if name in TIMM_FEATURE_SPECS: | |
| return load_timm_feature(name, dtype=dtype) | |
| if name in CUSTOM_SPECS: | |
| return load_custom_tower(name, dtype=torch.bfloat16) | |
| raise KeyError(f"unknown encoder {name!r}") | |