| """Tier A: free surrogate search (klaus-3 models). No API cost. |
| |
| Stage 1 coarse grid over structural knobs, then Stage 2 evolutionary/hill-climb |
| over the fine knobs, scored by surrogate label distance-to-truth with scraper |
| preprocessing baked in and stealth as a soft penalty. Returns gate-passing |
| candidates ranked by surrogate fitness. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import random |
| from concurrent.futures import ThreadPoolExecutor |
|
|
| from PIL import Image |
|
|
| from veil_pgd.config import Settings |
| from veil_pgd.fitness.embed import Embedder |
| from veil_pgd.fitness.objective import combined_fitness |
| from veil_pgd.fitness.semantic import aggregate, embedding_distance |
| from veil_pgd.optimizer import search_space as ss |
| from veil_pgd.render import render |
| from veil_pgd.robustness import scraper_sim |
| from veil_pgd.stealth import evaluate_stealth |
| from veil_pgd.targets.base import LabelPrompt |
| from veil_pgd.types import Candidate, RenderSpec |
|
|
|
|
| def cfg_workers(settings: Settings) -> int: |
| """Concurrency for cross-spec scoring. Bounded so we don't blow the shared |
| GPU's per-slot KV cache on klaus-3.""" |
| return max(2, int(getattr(settings.optimizer, "tier_a_workers", 4))) |
|
|
|
|
| class TierA: |
| def __init__( |
| self, |
| settings: Settings, |
| surrogates: list, |
| clip, |
| embedder: Embedder, |
| lpips_fn=None, |
| stealth_thresholds=None, |
| ): |
| self.s = settings |
| self.surrogates = surrogates |
| self.clip = clip |
| self.embedder = embedder |
| self.lpips_fn = lpips_fn |
| self.thresholds = stealth_thresholds or settings.stealth |
| self.prompt = LabelPrompt() |
| |
| |
| |
| self._pool = ThreadPoolExecutor(max_workers=cfg_workers(settings)) |
|
|
| def _score_spec(self, image: Image.Image, spec: RenderSpec, truth: str) -> Candidate: |
| rendered = render(image, spec) |
| probe = scraper_sim(rendered) |
|
|
| dists: list[float] = [] |
| preds: dict[str, str] = {} |
| for m in self.surrogates: |
| res = m.label(probe, self.prompt) |
| preds[m.name] = res.parsed_label |
| dists.append(embedding_distance(self.embedder, res.parsed_label, truth)) |
| distance = aggregate(dists, self.s.aggregation) |
|
|
| |
| reach_vals = [ |
| 1.0 - embedding_distance(self.embedder, p, spec.text) for p in preds.values() |
| ] |
| reachability = aggregate(reach_vals, "mean") if reach_vals else 0.0 |
|
|
| |
| stealth = evaluate_stealth( |
| image, rendered, thresholds=self.thresholds, lpips_fn=self.lpips_fn |
| ) |
|
|
| fitness = combined_fitness(distance, reachability, stealth, self.thresholds) |
| return Candidate( |
| spec=spec, |
| surrogate_fitness=fitness, |
| per_model_distance={k: d for k, d in zip(preds, dists)}, |
| reachability=reachability, |
| stealth=stealth, |
| notes={"surrogate_preds": preds, "distance_to_truth": distance}, |
| ) |
|
|
| def search( |
| self, image: Image.Image, truth: str, decoys: list[str] |
| ) -> list[Candidate]: |
| cfg = self.s.optimizer |
| rng = random.Random(0) |
|
|
| |
| positions = ss.default_positions() |
| specs = ss.grid_specs(decoys, positions, cfg) |
|
|
| scored = list( |
| self._pool.map(lambda sp: self._score_spec(image, sp, truth), specs) |
| ) |
| scored.sort(key=lambda c: c.surrogate_fitness, reverse=True) |
|
|
| |
| population = scored[: cfg.evo_population] |
| best = population[0].surrogate_fitness if population else 0.0 |
| stale = 0 |
| for _ in range(cfg.evo_generations): |
| child_specs = [ss.mutate(p.spec, cfg, rng) for p in population] |
| children = list( |
| self._pool.map(lambda sp: self._score_spec(image, sp, truth), child_specs) |
| ) |
| population = sorted( |
| population + children, key=lambda c: c.surrogate_fitness, reverse=True |
| )[: cfg.evo_population] |
| top = population[0].surrogate_fitness |
| if top - best < cfg.early_stop_epsilon: |
| stale += 1 |
| if stale >= cfg.early_stop_patience: |
| break |
| else: |
| stale = 0 |
| best = max(best, top) |
|
|
| |
| survivors = [c for c in population if c.stealth and c.stealth.passed] |
| survivors.sort(key=lambda c: c.surrogate_fitness, reverse=True) |
| return survivors[: self.s.budget.top_k_to_validate] |
|
|
| def close(self) -> None: |
| self._pool.shutdown(wait=False, cancel_futures=True) |
|
|