Spaces:
Sleeping
Sleeping
Download src/evaluation/runner.py from REAL-Lab-Imperial/eval-unlearn: direct link, hf CLI and curl.
- Browser
- Download file 9.69 kB
-
https://huggingface.co/spaces/REAL-Lab-Imperial/eval-unlearn/resolve/main/src/evaluation/runner.py
- Command line
-
hf download hf://spaces/REAL-Lab-Imperial/eval-unlearn/src/evaluation/runner.py
-
curl -L -o runner.py https://huggingface.co/spaces/REAL-Lab-Imperial/eval-unlearn/resolve/main/src/evaluation/runner.py
9.69 kB
| """Drives eval-unlearn's registry directly (bypassing SingleBenchmarkRunner / | |
| MultiBenchmarkRunner) so that: | |
| 1. The free_run diffusion pipeline is loaded once per submission and reused | |
| across all metrics (load_technique + run_one_metric), rather than | |
| reloaded per metric. On this Space's dedicated T4 hardware, reloading it | |
| 6x per submission churned enough GPU memory across CUDA allocator | |
| fragmentation to cause an out-of-memory crash a few metrics in — a hard | |
| crash, not a catchable Python exception, which killed the whole process | |
| and looked like the app "randomly refreshing" mid-submission. (This | |
| traded off against ZeroGPU's earlier per-call duration ceiling, which | |
| favored short-lived, independent calls — if this Space ever moves back | |
| to ZeroGPU, that tradeoff needs revisiting: a technique object loaded in | |
| one @spaces.GPU call may not be valid to reuse in another.) | |
| 2. Generated images are never written to disk — technique.generate() -> | |
| metric.update() -> metric.compute() happens in memory and the images are | |
| discarded, so no i2p-prompted imagery is ever persisted or published. | |
| "cheap" tier caps (num_inference_steps and each metric's `limit`) trade score | |
| precision for speed; the eval-unlearn CLI run with full dataset sizes remains | |
| the source of truth for publication-quality numbers. | |
| """ | |
| import os | |
| import sys | |
| import types | |
| from pathlib import Path | |
| from typing import Any, Dict, List, Optional | |
| import spaces | |
| def _stub_missing_technique_packages() -> None: | |
| """Work around a bug in eval-unlearn's own packaging. | |
| eval_unlearn/techniques/__init__.py unconditionally imports the uce, | |
| concept_steerers and mace wrapper modules (unlike esd/saeuron/safree/etc, | |
| which are correctly wrapped in try/except there). Each of those three | |
| unconditionally does `from uce import UCEPipeline` for an external | |
| package that was never published alongside eval-unlearn on PyPI. Since | |
| Python must run that __init__.py before importing *any* technique | |
| submodule, one missing unrelated package breaks free_run's registration | |
| too. We never use uce/concept_steerers/mace, so a harmless stub module is | |
| enough to let the parent package finish initializing. | |
| """ | |
| stub_attrs = { | |
| "uce": "UCEPipeline", | |
| "concept_steerers": "ConceptSteeringPipeline", | |
| "mace": "MACEPipeline", | |
| } | |
| for name, attr in stub_attrs.items(): | |
| if name in sys.modules: | |
| continue | |
| try: | |
| __import__(name) | |
| except ImportError: | |
| stub = types.ModuleType(name) | |
| setattr(stub, attr, object) | |
| sys.modules[name] = stub | |
| _stub_missing_technique_packages() | |
| DATA_DIR = Path(__file__).resolve().parent.parent.parent / "data" / "ua_ira" | |
| # The 7 concepts I2P has prompt categories for (datasets/_i2p_categories.py in | |
| # eval-unlearn). ASR-I2P is only ever non-N/A for these; "other" always N/A's. | |
| I2P_CONCEPTS = [ | |
| "nudity", | |
| "harassment", | |
| "hate", | |
| "illegal activity", | |
| "self-harm", | |
| "shocking", | |
| "violence", | |
| ] | |
| CONCEPTS = I2P_CONCEPTS + ["other"] | |
| # Metrics offered on the online "cheap" tier, in run order (cheapest/most | |
| # broadly-applicable first so a submitter sees partial progress quickly). | |
| METRIC_NAMES = ["clip_score", "asr_i2p", "ua_ira", "err", "tifa", "fid"] | |
| DEFAULT_NUM_INFERENCE_STEPS = 25 | |
| # Concepts eval-unlearn ships curated UA/IRA target/retain CSVs for. UA/IRA | |
| # can't be derived from a concept name alone (it needs a curated retain set), | |
| # so it's only offered for concepts we actually have CSVs for. | |
| UA_IRA_CONCEPTS = {"nudity", "violence"} | |
| # What the bundled retain CSV for each concept actually depicts, used as the | |
| # CLIP retain-concept label (see examples/data/{concept}_retain_prompts.csv). | |
| _UA_IRA_RETAIN_LABELS = {"nudity": "clothed person", "violence": "peaceful scene"} | |
| # Default number of images/prompts each metric uses on the "cheap" tier, and | |
| # the largest size the Submit tab's sliders allow. Sizes above the default | |
| # trade speed for precision — going too high risks exceeding a single | |
| # ZeroGPU call's time budget (run_one_metric's duration=180s). | |
| DEFAULT_SIZES: Dict[str, int] = { | |
| "clip_score": 25, | |
| "fid": 50, | |
| "tifa": 20, | |
| "asr_i2p": 15, | |
| "err": 15, # applied to each of target/retain/adversarial | |
| "ua_ira": 11, # the bundled curated CSVs only have 11 rows each | |
| } | |
| MAX_SIZES: Dict[str, int] = { | |
| "clip_score": 100, | |
| "fid": 150, | |
| "tifa": 60, | |
| "asr_i2p": 60, | |
| "err": 40, | |
| "ua_ira": 11, | |
| } | |
| def available_metrics_for_concept(concept: str) -> List[str]: | |
| """Which of METRIC_NAMES can actually run for this concept. | |
| err's forgetting check is hardcoded to nudity I2P prompts regardless of | |
| the technique's real concept (eval_unlearn.datasets.err_composite), so we | |
| only offer it for nudity submissions to avoid a misleading score. ua_ira | |
| needs curated retain prompts we only have for nudity/violence. | |
| """ | |
| metrics = list(METRIC_NAMES) | |
| if concept != "nudity": | |
| metrics.remove("err") | |
| if concept not in UA_IRA_CONCEPTS: | |
| metrics.remove("ua_ira") | |
| return metrics | |
| def _build_metric_config( | |
| metric_name: str, concept: str, size: Optional[int] = None | |
| ) -> Dict[str, Any]: | |
| """Build a metric's config dict, applying either its default size or a | |
| caller-supplied override (clamped to MAX_SIZES so the Submit tab's | |
| sliders can't push a metric past what a single ZeroGPU call can finish). | |
| """ | |
| n = size or DEFAULT_SIZES.get(metric_name) | |
| if n is not None: | |
| n = max(1, min(n, MAX_SIZES.get(metric_name, n))) | |
| config: Dict[str, Any] = {} | |
| if metric_name in ("clip_score", "fid", "tifa"): | |
| config["limit"] = n | |
| elif metric_name == "asr_i2p": | |
| config["limit"] = n | |
| config["concept_name"] = concept | |
| # asr_i2p's "auto" detector picks NudeNet for nudity and Q16 for | |
| # everything else — Q16 is an unpublished local package this Space | |
| # doesn't install. Force NudeNet for nudity (installed via the `asr` | |
| # extra) and fall back to the CLIP-similarity detector (needs only | |
| # `transformers`, already a dependency) for the other 6 concepts. | |
| config["detector"] = "nudenet" if concept == "nudity" else "clip" | |
| elif metric_name == "err": | |
| config.update({"target_limit": n, "retain_limit": n, "adversarial_limit": n}) | |
| elif metric_name == "ua_ira": | |
| config.update( | |
| { | |
| "target_prompts_path": str(DATA_DIR / f"{concept}_target_prompts.csv"), | |
| "retain_prompts_path": str(DATA_DIR / f"{concept}_retain_prompts.csv"), | |
| "target_concept": concept, | |
| "retain_concept": _UA_IRA_RETAIN_LABELS.get(concept, "retain"), | |
| "target_prompt_limit": n, | |
| "retain_prompt_limit": n, | |
| } | |
| ) | |
| return config | |
| def _metric_result_to_dict(result) -> Dict[str, Any]: | |
| return {"name": result.name, "value": result.value, "details": result.details} | |
| def load_technique(model_id: str, num_inference_steps: int = DEFAULT_NUM_INFERENCE_STEPS): | |
| """Load the free_run diffusion pipeline once for the whole submission. | |
| Raises on failure (bad model_id, load error) — the caller should abort | |
| the submission rather than retrying this once per metric. | |
| """ | |
| from eval_unlearn.registry import get_technique | |
| from eval_unlearn.registry.entrypoints import load_entrypoints | |
| load_entrypoints() | |
| technique_factory = get_technique("free_run") | |
| return technique_factory(model_id=model_id, num_inference_steps=num_inference_steps) | |
| def free_gpu_memory() -> None: | |
| """Call after the caller has `del`'d its own references (e.g. the | |
| technique) to actually release that memory back to the CUDA allocator. | |
| """ | |
| try: | |
| import torch | |
| torch.cuda.empty_cache() | |
| except Exception: | |
| pass | |
| def run_one_metric( | |
| technique, | |
| metric_name: str, | |
| concept: str, | |
| size: Optional[int] = None, | |
| seed: Optional[int] = 0, | |
| ) -> Dict[str, Any]: | |
| """Run a single metric against an already-loaded free_run technique. | |
| `size` overrides that metric's default number of images/prompts (see | |
| DEFAULT_SIZES/MAX_SIZES); leave it None to use the default "cheap" tier | |
| size. | |
| Returns a plain dict (not MetricResult) — always, even on failure, with an | |
| "error" key set instead of raising, so a caller looping over several | |
| metrics can record partial results instead of aborting the whole | |
| submission when one metric fails. | |
| """ | |
| from eval_unlearn.registry import get_metric | |
| from eval_unlearn.registry.entrypoints import load_entrypoints | |
| load_entrypoints() | |
| metric_config = _build_metric_config(metric_name, concept, size) | |
| metric = None | |
| try: | |
| metric_factory = get_metric(metric_name) | |
| metric = metric_factory(**metric_config) | |
| loader = metric.load_dataset() | |
| total = 0 | |
| for batch in loader: | |
| images = technique.generate(prompts=batch.prompts, seed=seed) | |
| metric.update(images, batch.prompts, batch.metadata) | |
| total += len(images) | |
| del images | |
| result = metric.compute() | |
| out = _metric_result_to_dict(result) | |
| out["details"] = {**out.get("details", {}), "images_generated": total} | |
| return out | |
| except Exception as e: # noqa: BLE001 - reported to the caller, not raised | |
| return {"name": metric_name, "value": None, "details": {}, "error": str(e)} | |
| finally: | |
| del metric | |
| free_gpu_memory() | |