Spaces:
Sleeping
Sleeping
File size: 9,685 Bytes
4690b6b 9f02a0d 4690b6b e462b24 4690b6b e462b24 4690b6b a243c9c 4690b6b a243c9c 4690b6b a243c9c 4690b6b a243c9c 4690b6b a243c9c 4690b6b 9f02a0d 4690b6b 9f02a0d 4690b6b a243c9c 4690b6b 9f02a0d 4690b6b a243c9c 4690b6b 9f02a0d 4690b6b a243c9c 4690b6b 9f02a0d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | """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}
@spaces.GPU(duration=60)
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
@spaces.GPU(duration=180)
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()
|