pliny-the-prompter commited on
Commit
2b4c8be
Β·
verified Β·
1 Parent(s): e091ad1

Upload 133 files

Browse files
Files changed (3) hide show
  1. app.py +172 -99
  2. obliteratus/abliterate.py +140 -0
  3. obliteratus/informed_pipeline.py +27 -0
app.py CHANGED
@@ -2046,7 +2046,22 @@ def _format_multi_model_results(results: list[dict], context: dict | None = None
2046
  return "\n".join(lines)
2047
 
2048
 
 
 
 
 
2049
  @spaces.GPU(duration=300)
 
 
 
 
 
 
 
 
 
 
 
2050
  def obliterate(model_choice: str, method_choice: str,
2051
  prompt_volume_choice: str, dataset_source_choice: str,
2052
  custom_harmful: str, custom_harmless: str,
@@ -2077,9 +2092,14 @@ def obliterate(model_choice: str, method_choice: str,
2077
  progress=gr.Progress()):
2078
  """Run the full obliteration pipeline, streaming log updates to the UI.
2079
 
2080
- On ZeroGPU Spaces, this function runs on the visitor's GPU quota (up to
2081
- 5 minutes). The @spaces.GPU decorator allocates a GPU at call time and
2082
- releases it when the function returns.
 
 
 
 
 
2083
  """
2084
  import os
2085
  import re
@@ -2188,106 +2208,159 @@ def obliterate(model_choice: str, method_choice: str,
2188
 
2189
  quantization = _should_quantize(model_id, is_preset=is_preset)
2190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2191
  def run_pipeline():
2192
  try:
2193
- _t_pipeline_start = time.time()
2194
- on_log(f"[timing] Pipeline thread started (ZeroGPU allocation: {300}s)")
2195
- if _ZEROGPU_AVAILABLE:
2196
- on_log("[timing] Running on ZeroGPU β€” GPU may be deallocated if pipeline exceeds duration")
2197
 
2198
- # Load prompts β€” custom overrides dataset dropdown
2199
- if use_custom:
2200
- on_log("Using custom user-provided prompts...")
2201
- harmful_all, harmless_all = load_custom_prompts(
2202
- custom_harmful, custom_harmless or "",
2203
- )
2204
- on_log(f"Custom prompts: {len(harmful_all)} harmful, {len(harmless_all)} harmless")
2205
- else:
2206
- on_log(f"Loading dataset: {dataset_key}...")
2207
- harmful_all, harmless_all = load_dataset_source(dataset_key)
2208
- on_log(f"Dataset loaded: {len(harmful_all)} harmful, {len(harmless_all)} harmless prompts")
2209
- on_log(f"[timing] Dataset loaded at +{time.time() - _t_pipeline_start:.1f}s")
2210
-
2211
- # Apply volume cap (-1 = use all)
2212
- if prompt_volume > 0:
2213
- n = min(prompt_volume, len(harmful_all), len(harmless_all))
2214
- else:
2215
- n = min(len(harmful_all), len(harmless_all))
2216
-
2217
- if method == "informed":
2218
- # Use the analysis-guided InformedAbliterationPipeline
2219
- from obliteratus.informed_pipeline import InformedAbliterationPipeline
2220
- pipeline = InformedAbliterationPipeline(
2221
- model_name=model_id,
2222
- output_dir=save_dir,
2223
- device="auto",
2224
- dtype="float16",
2225
- quantization=quantization,
2226
- trust_remote_code=is_preset,
2227
- harmful_prompts=harmful_all[:n],
2228
- harmless_prompts=harmless_all[:n],
2229
- on_stage=on_stage,
2230
- on_log=on_log,
2231
- )
2232
- pipeline._bayesian_trials = int(adv_bayesian_trials)
2233
- pipeline_ref[0] = pipeline
2234
- pipeline.run_informed(gpu_start_time=t_start)
 
 
 
 
 
 
 
 
 
 
2235
  else:
2236
- from obliteratus.abliterate import AbliterationPipeline
2237
- pipeline = AbliterationPipeline(
2238
- model_name=model_id,
2239
- output_dir=save_dir,
2240
- device="auto",
2241
- dtype="float16",
2242
- method=method,
2243
- quantization=quantization,
2244
- trust_remote_code=is_preset,
2245
- harmful_prompts=harmful_all[:n],
2246
- harmless_prompts=harmless_all[:n],
2247
- on_stage=on_stage,
2248
- on_log=on_log,
2249
- # Advanced overrides from UI
2250
- n_directions=int(adv_n_directions),
2251
- direction_method=adv_direction_method,
2252
- regularization=float(adv_regularization),
2253
- refinement_passes=int(adv_refinement_passes),
2254
- norm_preserve=adv_norm_preserve,
2255
- project_biases=adv_project_biases,
2256
- use_chat_template=adv_use_chat_template,
2257
- use_whitened_svd=adv_use_whitened_svd,
2258
- true_iterative_refinement=adv_true_iterative,
2259
- use_jailbreak_contrast=adv_jailbreak_contrast,
2260
- layer_adaptive_strength=adv_layer_adaptive,
2261
- safety_neuron_masking=adv_safety_neuron,
2262
- per_expert_directions=adv_per_expert,
2263
- attention_head_surgery=adv_attn_surgery,
2264
- use_sae_features=adv_sae_features,
2265
- invert_refusal=adv_invert_refusal,
2266
- reflection_strength=float(adv_reflection_strength),
2267
- project_embeddings=adv_project_embeddings,
2268
- embed_regularization=float(adv_embed_regularization),
2269
- activation_steering=adv_activation_steering,
2270
- steering_strength=float(adv_steering_strength),
2271
- expert_transplant=adv_expert_transplant,
2272
- transplant_blend=float(adv_transplant_blend),
2273
- use_wasserstein_optimal=adv_wasserstein_optimal,
2274
- spectral_cascade=adv_spectral_cascade,
2275
- spectral_bands=int(adv_spectral_bands),
2276
- spectral_threshold=float(adv_spectral_threshold),
2277
- verify_sample_size=int(adv_verify_sample_size),
2278
- layer_selection=adv_layer_selection,
2279
- winsorize_activations=adv_winsorize,
2280
- winsorize_percentile=float(adv_winsorize_percentile),
2281
- use_kl_optimization=adv_kl_optimization,
2282
- kl_budget=float(adv_kl_budget),
2283
- float_layer_interpolation=adv_float_layer_interp,
2284
- rdo_refinement=adv_rdo_refinement,
2285
- cot_aware=adv_cot_aware,
2286
- n_sae_features=int(adv_n_sae_features),
2287
- )
2288
- pipeline._bayesian_trials = int(adv_bayesian_trials)
2289
- pipeline_ref[0] = pipeline
2290
- pipeline.run(gpu_start_time=t_start)
2291
  except Exception as e:
2292
  error_ref[0] = e
2293
  tb = traceback.format_exc()
 
2046
  return "\n".join(lines)
2047
 
2048
 
2049
+ # ---------------------------------------------------------------------------
2050
+ # Staged GPU wrapper for obliteration (tourney-style per-stage allocation)
2051
+ # ---------------------------------------------------------------------------
2052
+
2053
  @spaces.GPU(duration=300)
2054
+ def _obliterate_gpu_run(fn, *args, **kwargs):
2055
+ """Execute *fn* inside a ZeroGPU GPU allocation.
2056
+
2057
+ Used by ``obliterate`` to give each pipeline stage its own 5-minute
2058
+ GPU allocation instead of sharing a single allocation for the whole
2059
+ pipeline. On non-ZeroGPU machines the ``@spaces.GPU`` decorator is a
2060
+ no-op and this simply calls *fn* directly.
2061
+ """
2062
+ return fn(*args, **kwargs)
2063
+
2064
+
2065
  def obliterate(model_choice: str, method_choice: str,
2066
  prompt_volume_choice: str, dataset_source_choice: str,
2067
  custom_harmful: str, custom_harmless: str,
 
2092
  progress=gr.Progress()):
2093
  """Run the full obliteration pipeline, streaming log updates to the UI.
2094
 
2095
+ On ZeroGPU Spaces, the pipeline is split into 3 GPU stages (up to 5 min
2096
+ each) using the tourney-style approach: each stage gets its own
2097
+ ``@spaces.GPU(duration=300)`` allocation via ``_obliterate_gpu_run``.
2098
+ Between stages the model is offloaded to CPU and the GPU is released,
2099
+ preventing the 5-minute ZeroGPU timeout from killing large-model runs.
2100
+
2101
+ On local/non-ZeroGPU machines, the pipeline runs in a single shot as
2102
+ before (no time limit).
2103
  """
2104
  import os
2105
  import re
 
2208
 
2209
  quantization = _should_quantize(model_id, is_preset=is_preset)
2210
 
2211
+ def _create_pipeline(on_log, on_stage):
2212
+ """Create the pipeline object and load prompts (no GPU required)."""
2213
+ _t_pipeline_start = time.time()
2214
+
2215
+ # Load prompts β€” custom overrides dataset dropdown
2216
+ if use_custom:
2217
+ on_log("Using custom user-provided prompts...")
2218
+ harmful_all, harmless_all = load_custom_prompts(
2219
+ custom_harmful, custom_harmless or "",
2220
+ )
2221
+ on_log(f"Custom prompts: {len(harmful_all)} harmful, {len(harmless_all)} harmless")
2222
+ else:
2223
+ on_log(f"Loading dataset: {dataset_key}...")
2224
+ harmful_all, harmless_all = load_dataset_source(dataset_key)
2225
+ on_log(f"Dataset loaded: {len(harmful_all)} harmful, {len(harmless_all)} harmless prompts")
2226
+ on_log(f"[timing] Dataset loaded at +{time.time() - _t_pipeline_start:.1f}s")
2227
+
2228
+ # Apply volume cap (-1 = use all)
2229
+ if prompt_volume > 0:
2230
+ n = min(prompt_volume, len(harmful_all), len(harmless_all))
2231
+ else:
2232
+ n = min(len(harmful_all), len(harmless_all))
2233
+
2234
+ if method == "informed":
2235
+ from obliteratus.informed_pipeline import InformedAbliterationPipeline
2236
+ pipeline = InformedAbliterationPipeline(
2237
+ model_name=model_id,
2238
+ output_dir=save_dir,
2239
+ device="auto",
2240
+ dtype="float16",
2241
+ quantization=quantization,
2242
+ trust_remote_code=is_preset,
2243
+ harmful_prompts=harmful_all[:n],
2244
+ harmless_prompts=harmless_all[:n],
2245
+ on_stage=on_stage,
2246
+ on_log=on_log,
2247
+ )
2248
+ else:
2249
+ from obliteratus.abliterate import AbliterationPipeline
2250
+ pipeline = AbliterationPipeline(
2251
+ model_name=model_id,
2252
+ output_dir=save_dir,
2253
+ device="auto",
2254
+ dtype="float16",
2255
+ method=method,
2256
+ quantization=quantization,
2257
+ trust_remote_code=is_preset,
2258
+ harmful_prompts=harmful_all[:n],
2259
+ harmless_prompts=harmless_all[:n],
2260
+ on_stage=on_stage,
2261
+ on_log=on_log,
2262
+ # Advanced overrides from UI
2263
+ n_directions=int(adv_n_directions),
2264
+ direction_method=adv_direction_method,
2265
+ regularization=float(adv_regularization),
2266
+ refinement_passes=int(adv_refinement_passes),
2267
+ norm_preserve=adv_norm_preserve,
2268
+ project_biases=adv_project_biases,
2269
+ use_chat_template=adv_use_chat_template,
2270
+ use_whitened_svd=adv_use_whitened_svd,
2271
+ true_iterative_refinement=adv_true_iterative,
2272
+ use_jailbreak_contrast=adv_jailbreak_contrast,
2273
+ layer_adaptive_strength=adv_layer_adaptive,
2274
+ safety_neuron_masking=adv_safety_neuron,
2275
+ per_expert_directions=adv_per_expert,
2276
+ attention_head_surgery=adv_attn_surgery,
2277
+ use_sae_features=adv_sae_features,
2278
+ invert_refusal=adv_invert_refusal,
2279
+ reflection_strength=float(adv_reflection_strength),
2280
+ project_embeddings=adv_project_embeddings,
2281
+ embed_regularization=float(adv_embed_regularization),
2282
+ activation_steering=adv_activation_steering,
2283
+ steering_strength=float(adv_steering_strength),
2284
+ expert_transplant=adv_expert_transplant,
2285
+ transplant_blend=float(adv_transplant_blend),
2286
+ use_wasserstein_optimal=adv_wasserstein_optimal,
2287
+ spectral_cascade=adv_spectral_cascade,
2288
+ spectral_bands=int(adv_spectral_bands),
2289
+ spectral_threshold=float(adv_spectral_threshold),
2290
+ verify_sample_size=int(adv_verify_sample_size),
2291
+ layer_selection=adv_layer_selection,
2292
+ winsorize_activations=adv_winsorize,
2293
+ winsorize_percentile=float(adv_winsorize_percentile),
2294
+ use_kl_optimization=adv_kl_optimization,
2295
+ kl_budget=float(adv_kl_budget),
2296
+ float_layer_interpolation=adv_float_layer_interp,
2297
+ rdo_refinement=adv_rdo_refinement,
2298
+ cot_aware=adv_cot_aware,
2299
+ n_sae_features=int(adv_n_sae_features),
2300
+ )
2301
+ pipeline._bayesian_trials = int(adv_bayesian_trials)
2302
+ return pipeline
2303
+
2304
  def run_pipeline():
2305
  try:
2306
+ on_log(f"[timing] Pipeline thread started")
2307
+ pipeline = _create_pipeline(on_log, on_stage)
2308
+ pipeline_ref[0] = pipeline
 
2309
 
2310
+ if _ZEROGPU_AVAILABLE:
2311
+ # ── Staged GPU execution (tourney-style) ──────────────────
2312
+ # Each stage gets its own 5-minute GPU allocation instead of
2313
+ # sharing a single 300s budget. Between stages the model is
2314
+ # moved to CPU and the GPU is released.
2315
+ on_log("[staged] ZeroGPU detected β€” using staged GPU execution (up to 5 min per stage)")
2316
+
2317
+ if method == "informed":
2318
+ # Informed pipeline: SUMMON+PROBE | ANALYZE+DISTILL+EXCISE | VERIFY+REBIRTH
2319
+ on_log("\n\u26a1 [staged] GPU Stage 1/3: SUMMON + PROBE")
2320
+ _obliterate_gpu_run(pipeline.run_stage_summon_probe, time.time())
2321
+ pipeline._offload_to_cpu()
2322
+ on_log("[staged] GPU released after Stage 1\n")
2323
+
2324
+ on_log("\u26a1 [staged] GPU Stage 2/3: ANALYZE + DISTILL + EXCISE")
2325
+ def _informed_s2():
2326
+ pipeline._restore_to_gpu()
2327
+ pipeline.run_stage_analyze_distill_excise()
2328
+ _obliterate_gpu_run(_informed_s2)
2329
+ pipeline._offload_to_cpu()
2330
+ on_log("[staged] GPU released after Stage 2\n")
2331
+
2332
+ on_log("\u26a1 [staged] GPU Stage 3/3: VERIFY + REBIRTH")
2333
+ def _informed_s3():
2334
+ pipeline._restore_to_gpu()
2335
+ pipeline.run_stage_verify_rebirth_informed()
2336
+ _obliterate_gpu_run(_informed_s3)
2337
+ else:
2338
+ # Standard pipeline: SUMMON+PROBE | DISTILL+EXCISE | VERIFY+REBIRTH
2339
+ on_log("\n\u26a1 [staged] GPU Stage 1/3: SUMMON + PROBE")
2340
+ _obliterate_gpu_run(pipeline.run_stage_summon_probe, time.time())
2341
+ pipeline._offload_to_cpu()
2342
+ on_log("[staged] GPU released after Stage 1\n")
2343
+
2344
+ on_log("\u26a1 [staged] GPU Stage 2/3: DISTILL + EXCISE")
2345
+ def _standard_s2():
2346
+ pipeline._restore_to_gpu()
2347
+ pipeline.run_stage_distill_excise()
2348
+ _obliterate_gpu_run(_standard_s2)
2349
+ pipeline._offload_to_cpu()
2350
+ on_log("[staged] GPU released after Stage 2\n")
2351
+
2352
+ on_log("\u26a1 [staged] GPU Stage 3/3: VERIFY + REBIRTH")
2353
+ def _standard_s3():
2354
+ pipeline._restore_to_gpu()
2355
+ pipeline.run_stage_verify_rebirth()
2356
+ _obliterate_gpu_run(_standard_s3)
2357
  else:
2358
+ # ── Local/non-ZeroGPU: single-shot execution ──────────────
2359
+ on_log(f"[timing] Running locally (no GPU time limit)")
2360
+ if method == "informed":
2361
+ pipeline.run_informed(gpu_start_time=t_start)
2362
+ else:
2363
+ pipeline.run(gpu_start_time=t_start)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2364
  except Exception as e:
2365
  error_ref[0] = e
2366
  tb = traceback.format_exc()
obliteratus/abliterate.py CHANGED
@@ -985,6 +985,146 @@ class AbliterationPipeline:
985
  self.log(f"[timing] REBIRTH complete at +{time.time() - _t0:.1f}s β€” pipeline finished")
986
  return result
987
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
988
  # ── Stage 1: SUMMON ─────────────────────────────────────────────────
989
 
990
  def _summon(self):
 
985
  self.log(f"[timing] REBIRTH complete at +{time.time() - _t0:.1f}s β€” pipeline finished")
986
  return result
987
 
988
+ # ── Staged execution (ZeroGPU-safe) ──────────────────────────────────
989
+ # These methods split the pipeline into independent GPU stages so each
990
+ # stage can run in its own @spaces.GPU allocation (up to 5 min each).
991
+ # Between stages the caller moves the model to CPU and releases the GPU.
992
+ # This mirrors the tourney approach and prevents ZeroGPU timeouts on
993
+ # large models that need more than 5 minutes total.
994
+
995
+ def run_stage_summon_probe(self, gpu_start_time: float | None = None):
996
+ """GPU Stage 1: Load model and collect activations (SUMMON + PROBE).
997
+
998
+ After this stage the caller should call ``_offload_to_cpu()`` to
999
+ move the model off GPU before releasing the ZeroGPU allocation.
1000
+ """
1001
+ for h in self._steering_hooks:
1002
+ h.remove()
1003
+ self._steering_hooks.clear()
1004
+ self._staged_t0 = time.time()
1005
+ self._pipeline_start_time = gpu_start_time if gpu_start_time is not None else self._staged_t0
1006
+ self._summon()
1007
+ self.log(f"[timing] SUMMON complete at +{time.time() - self._staged_t0:.1f}s")
1008
+ self._free_gpu_memory()
1009
+ self._probe()
1010
+ self.log(f"[timing] PROBE complete at +{time.time() - self._staged_t0:.1f}s")
1011
+ self._free_gpu_memory()
1012
+
1013
+ def run_stage_distill_excise(self):
1014
+ """GPU Stage 2: Extract directions, modify weights, save checkpoint
1015
+ (DISTILL + EXCISE + quick checkpoint).
1016
+
1017
+ After this stage the caller should call ``_offload_to_cpu()`` to
1018
+ move the model off GPU before releasing the ZeroGPU allocation.
1019
+ """
1020
+ # Reset time budget for this GPU allocation
1021
+ self._pipeline_start_time = time.time()
1022
+ self._distill()
1023
+ self.log(f"[timing] DISTILL complete at +{time.time() - self._staged_t0:.1f}s")
1024
+ # Free raw per-prompt activations now that means/subspaces are extracted
1025
+ self._harmful_acts.clear()
1026
+ self._harmless_acts.clear()
1027
+ self._jailbreak_acts.clear()
1028
+ self._harmful_means.clear()
1029
+ self._harmless_means.clear()
1030
+ self._routing_harmful.clear()
1031
+ self._routing_harmless.clear()
1032
+ self._free_gpu_memory()
1033
+ self._capture_baseline_kl_logits()
1034
+ self._excise()
1035
+ self.log(f"[timing] EXCISE complete at +{time.time() - self._staged_t0:.1f}s")
1036
+ self._free_gpu_memory()
1037
+ self._save_quick_checkpoint()
1038
+
1039
+ def run_stage_verify_rebirth(self) -> Path:
1040
+ """GPU Stage 3: Quality verification and final save (VERIFY + REBIRTH).
1041
+
1042
+ Returns:
1043
+ Path to the saved model directory.
1044
+ """
1045
+ # Reset time budget for this GPU allocation
1046
+ self._pipeline_start_time = time.time()
1047
+ self._verify()
1048
+ self.log(f"[timing] VERIFY complete at +{time.time() - self._staged_t0:.1f}s")
1049
+ self._free_gpu_memory()
1050
+ result = self._rebirth()
1051
+ self.log(f"[timing] REBIRTH complete at +{time.time() - self._staged_t0:.1f}s β€” pipeline finished")
1052
+ return result
1053
+
1054
+ def _offload_to_cpu(self):
1055
+ """Move model and intermediate tensors to CPU for ZeroGPU stage boundary.
1056
+
1057
+ Called between staged GPU allocations so the model survives GPU
1058
+ deallocation. Also moves cached activation tensors to CPU.
1059
+ """
1060
+ if self.handle is None or self.handle.model is None:
1061
+ return
1062
+ model = self.handle.model
1063
+ # Remember the GPU device for later restoration
1064
+ try:
1065
+ self._gpu_device = self._get_model_device(model)
1066
+ except (StopIteration, RuntimeError):
1067
+ self._gpu_device = torch.device("cuda:0")
1068
+
1069
+ self.log("[staged] Moving model to CPU between GPU allocations...")
1070
+ t0 = time.time()
1071
+ model.to("cpu")
1072
+
1073
+ # Move any cached tensors that are still on GPU
1074
+ for tensor_dict in (
1075
+ self._harmful_means, self._harmless_means,
1076
+ self._jailbreak_means,
1077
+ ):
1078
+ for k in list(tensor_dict.keys()):
1079
+ if isinstance(tensor_dict[k], torch.Tensor) and tensor_dict[k].device.type != "cpu":
1080
+ tensor_dict[k] = tensor_dict[k].cpu()
1081
+
1082
+ for act_dict in (self._harmful_acts, self._harmless_acts, self._jailbreak_acts):
1083
+ for k in list(act_dict.keys()):
1084
+ if isinstance(act_dict[k], list):
1085
+ act_dict[k] = [t.cpu() if isinstance(t, torch.Tensor) and t.device.type != "cpu" else t
1086
+ for t in act_dict[k]]
1087
+
1088
+ # Move refusal directions/subspaces to CPU
1089
+ for k in list(self.refusal_directions.keys()):
1090
+ v = self.refusal_directions[k]
1091
+ if isinstance(v, torch.Tensor) and v.device.type != "cpu":
1092
+ self.refusal_directions[k] = v.cpu()
1093
+ for k in list(self.refusal_subspaces.keys()):
1094
+ v = self.refusal_subspaces[k]
1095
+ if isinstance(v, torch.Tensor) and v.device.type != "cpu":
1096
+ self.refusal_subspaces[k] = v.cpu()
1097
+
1098
+ # Move baseline KL logits to CPU
1099
+ if hasattr(self, "_kl_baseline_logits") and self._kl_baseline_logits is not None:
1100
+ self._kl_baseline_logits = [
1101
+ t.cpu() if isinstance(t, torch.Tensor) and t.device.type != "cpu" else t
1102
+ for t in self._kl_baseline_logits
1103
+ ]
1104
+
1105
+ dev.free_gpu_memory()
1106
+ elapsed = time.time() - t0
1107
+ self.log(f"[staged] Model offloaded to CPU ({elapsed:.1f}s)")
1108
+
1109
+ def _restore_to_gpu(self):
1110
+ """Move model back to GPU at the start of a new GPU allocation.
1111
+
1112
+ Uses the device stored by ``_offload_to_cpu()``, defaulting to
1113
+ ``cuda:0`` if not set.
1114
+ """
1115
+ if self.handle is None or self.handle.model is None:
1116
+ return
1117
+ target = getattr(self, "_gpu_device", None) or torch.device("cuda:0")
1118
+ # On ZeroGPU, the device index may change between allocations,
1119
+ # so always use cuda:0 which is the only device available.
1120
+ if target.type == "cuda":
1121
+ target = torch.device("cuda:0")
1122
+ self.log(f"[staged] Moving model to {target}...")
1123
+ t0 = time.time()
1124
+ self.handle.model.to(target)
1125
+ elapsed = time.time() - t0
1126
+ self.log(f"[staged] Model restored to {target} ({elapsed:.1f}s)")
1127
+
1128
  # ── Stage 1: SUMMON ─────────────────────────────────────────────────
1129
 
1130
  def _summon(self):
obliteratus/informed_pipeline.py CHANGED
@@ -299,6 +299,33 @@ class InformedAbliterationPipeline(AbliterationPipeline):
299
  self._report.total_duration = time.time() - t0
300
  return output_path, self._report
301
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  # ── Stage 3: ANALYZE ─────────────────────────────────────────────
303
 
304
  def _analyze(self):
 
299
  self._report.total_duration = time.time() - t0
300
  return output_path, self._report
301
 
302
+ # ── Staged execution (ZeroGPU-safe) ──────────────────────────────
303
+ # Split the informed pipeline into 3 GPU stages so each gets its own
304
+ # @spaces.GPU(duration=300) allocation. SUMMON+PROBE is inherited
305
+ # from AbliterationPipeline.run_stage_summon_probe().
306
+
307
+ def run_stage_analyze_distill_excise(self):
308
+ """GPU Stage 2 (informed): ANALYZE + DISTILL + EXCISE + quick checkpoint."""
309
+ # Reset time budget for this GPU allocation
310
+ self._pipeline_start_time = time.time()
311
+ self._analyze()
312
+ self._distill_informed()
313
+ self._excise_informed()
314
+ self._save_quick_checkpoint()
315
+
316
+ def run_stage_verify_rebirth_informed(self):
317
+ """GPU Stage 3 (informed): VERIFY + Ouroboros compensation + REBIRTH.
318
+
319
+ Stores the result path and report in instance attributes so the
320
+ caller can retrieve them after the GPU allocation returns.
321
+ """
322
+ # Reset time budget for this GPU allocation
323
+ self._pipeline_start_time = time.time()
324
+ self._verify_and_compensate()
325
+ output_path = self._rebirth_informed()
326
+ self._report.total_duration = time.time() - getattr(self, "_staged_t0", time.time())
327
+ self._staged_result = (output_path, self._report)
328
+
329
  # ── Stage 3: ANALYZE ─────────────────────────────────────────────
330
 
331
  def _analyze(self):