mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-08-30 06:30:37 +02:00
fix(pipeline): fail closed on invalid output
This commit is contained in:
@@ -71,6 +71,7 @@ from obliteratus.model_load_settings import (
|
||||
ModelLoadSettings,
|
||||
resolve_model_load_settings,
|
||||
)
|
||||
from obliteratus.persistence_contracts import validate_reloadable_checkpoint
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
|
||||
|
||||
# ── ZeroGPU support ─────────────────────────────────────────────────
|
||||
@@ -121,6 +122,7 @@ def _load_model_to_device(
|
||||
offload_folder: str | None = None,
|
||||
low_cpu_mem_usage: bool = False,
|
||||
token: str | None = None,
|
||||
local_files_only: bool = False,
|
||||
) -> AutoModelForCausalLM:
|
||||
"""Load a causal LM onto the best available device, MPS-safe.
|
||||
|
||||
@@ -142,6 +144,8 @@ def _load_model_to_device(
|
||||
kwargs["low_cpu_mem_usage"] = True
|
||||
if token is not None:
|
||||
kwargs["token"] = token
|
||||
if local_files_only:
|
||||
kwargs["local_files_only"] = True
|
||||
|
||||
if dev.supports_device_map_auto():
|
||||
kwargs["device_map"] = "auto"
|
||||
@@ -156,6 +160,87 @@ def _load_model_to_device(
|
||||
return model
|
||||
|
||||
|
||||
def _resolve_local_checkpoint(checkpoint: str | Path) -> Path:
|
||||
"""Resolve and fully validate a local checkpoint before Transformers sees it."""
|
||||
return validate_reloadable_checkpoint(checkpoint)
|
||||
|
||||
|
||||
def _reload_local_checkpoint(
|
||||
checkpoint: str | Path,
|
||||
*,
|
||||
trust_remote_code: bool,
|
||||
model_kwargs: dict | None = None,
|
||||
):
|
||||
"""Load a validated local checkpoint without permitting Hub resolution."""
|
||||
checkpoint_path = _resolve_local_checkpoint(checkpoint)
|
||||
model = _load_model_to_device(
|
||||
checkpoint_path,
|
||||
trust_remote_code=trust_remote_code,
|
||||
local_files_only=True,
|
||||
**(model_kwargs or {}),
|
||||
)
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
checkpoint_path,
|
||||
trust_remote_code=trust_remote_code,
|
||||
local_files_only=True,
|
||||
)
|
||||
if tokenizer.pad_token is None:
|
||||
tokenizer.pad_token = tokenizer.eos_token
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
def _format_checkpoint_reload_error(error: BaseException) -> str:
|
||||
"""Return an actionable reload category without misreporting GPU capacity."""
|
||||
message = str(error) or repr(error)
|
||||
lowered = message.lower()
|
||||
if isinstance(error, FileNotFoundError):
|
||||
return f"Saved checkpoint is missing: {message}"
|
||||
if "corrupt" in lowered:
|
||||
return f"Saved checkpoint is corrupt: {message}"
|
||||
if isinstance(error, ValueError) and "checkpoint" in lowered:
|
||||
return f"Saved checkpoint is incomplete: {message}"
|
||||
if dev.is_oom_error(error):
|
||||
return f"Insufficient GPU memory to reload the completed checkpoint: {message}"
|
||||
if "repo id" in lowered or "repository" in lowered:
|
||||
return f"Checkpoint Hub resolution failed: {message}"
|
||||
return f"Completed checkpoint could not be reloaded: {message}"
|
||||
|
||||
|
||||
def _cancel_pipeline_worker(
|
||||
worker: threading.Thread,
|
||||
cancellation: threading.Event,
|
||||
*,
|
||||
grace_seconds: float,
|
||||
) -> bool:
|
||||
"""Signal cooperative cancellation and report confirmed worker termination."""
|
||||
cancellation.set()
|
||||
worker.join(timeout=grace_seconds)
|
||||
return not worker.is_alive()
|
||||
|
||||
|
||||
def _cleanup_failed_pipeline(pipeline, output_dir: str) -> None:
|
||||
"""Release failed model state and remove only this UI run's final artifact."""
|
||||
if pipeline is not None:
|
||||
try:
|
||||
pipeline.cleanup_failed_run()
|
||||
except Exception:
|
||||
pass
|
||||
path = Path(output_dir)
|
||||
try:
|
||||
resolved = path.resolve()
|
||||
temp_root = Path("/tmp").resolve()
|
||||
if (
|
||||
resolved.parent == temp_root
|
||||
and resolved.name.startswith("obliterated_")
|
||||
and resolved.is_dir()
|
||||
):
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(resolved, ignore_errors=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _checkpoint_load_kwargs(load_settings: ModelLoadSettings) -> dict:
|
||||
"""Translate resolved settings for direct loading of a saved checkpoint."""
|
||||
torch_dtype = {
|
||||
@@ -2072,6 +2157,7 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
last_yielded = [0]
|
||||
pipeline_ref = [None]
|
||||
error_ref = [None]
|
||||
cancellation = threading.Event()
|
||||
t_start = time.time()
|
||||
|
||||
def _elapsed():
|
||||
@@ -2128,6 +2214,7 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
harmless_prompts=harmless_all[:n],
|
||||
on_stage=on_stage,
|
||||
on_log=on_log,
|
||||
cancellation_event=cancellation,
|
||||
)
|
||||
pipeline_ref[0] = pipeline
|
||||
pipeline.run_informed()
|
||||
@@ -2145,6 +2232,7 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
harmless_prompts=harmless_all[:n],
|
||||
on_stage=on_stage,
|
||||
on_log=on_log,
|
||||
cancellation_event=cancellation,
|
||||
# Advanced overrides from UI
|
||||
n_directions=int(adv_n_directions),
|
||||
direction_method=adv_direction_method,
|
||||
@@ -2210,6 +2298,7 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
# Stream log updates while pipeline runs (max 45 minutes to prevent indefinite hang)
|
||||
_max_pipeline_secs = 45 * 60
|
||||
_pipeline_start = time.time()
|
||||
timed_out = False
|
||||
status_msg = "**Obliterating\u2026** (0s)"
|
||||
while worker.is_alive():
|
||||
status_msg = f"**Obliterating\u2026** ({_elapsed()})"
|
||||
@@ -2220,13 +2309,47 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
yield status_msg, "\n".join(log_lines), gr.update(), gr.update(), gr.update(), gr.update()
|
||||
if time.time() - _pipeline_start > _max_pipeline_secs:
|
||||
log_lines.append("\nTIMEOUT: Pipeline exceeded 45-minute limit.")
|
||||
cancellation.set()
|
||||
timed_out = True
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
worker.join(timeout=30)
|
||||
if timed_out:
|
||||
terminated = _cancel_pipeline_worker(
|
||||
worker,
|
||||
cancellation,
|
||||
grace_seconds=30,
|
||||
)
|
||||
pipeline = pipeline_ref[0]
|
||||
if terminated:
|
||||
_cleanup_failed_pipeline(pipeline, save_dir)
|
||||
detail = "Worker stopped; incomplete output was discarded."
|
||||
else:
|
||||
detail = (
|
||||
"Worker is still stopping cooperatively; no output or session state "
|
||||
"will be exposed."
|
||||
)
|
||||
with _lock:
|
||||
_state["status"] = "idle"
|
||||
_state["model"] = None
|
||||
_state["tokenizer"] = None
|
||||
log_lines.append(detail)
|
||||
_state["log"] = log_lines
|
||||
yield (
|
||||
"**Timed out:** Pipeline cancelled; no successful artifact was registered.",
|
||||
"\n".join(log_lines),
|
||||
get_chat_header(),
|
||||
gr.update(),
|
||||
gr.update(),
|
||||
gr.update(),
|
||||
)
|
||||
return
|
||||
|
||||
worker.join()
|
||||
|
||||
# Handle error
|
||||
if error_ref[0] is not None:
|
||||
_cleanup_failed_pipeline(pipeline_ref[0], save_dir)
|
||||
with _lock:
|
||||
_state["status"] = "idle"
|
||||
err_msg = str(error_ref[0]) or repr(error_ref[0])
|
||||
@@ -2239,6 +2362,7 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
# Wrapped in try/except to ensure status is never stuck on "obliterating".
|
||||
try:
|
||||
pipeline = pipeline_ref[0]
|
||||
save_dir = str(_resolve_local_checkpoint(save_dir))
|
||||
can_generate = pipeline._quality_metrics.get("coherence") is not None
|
||||
|
||||
# ── Telemetry: log single obliteration to community leaderboard ──
|
||||
@@ -2345,8 +2469,17 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
dtype_label = {
|
||||
"float16": "FP16",
|
||||
"bfloat16": "BF16",
|
||||
"float32": "FP32",
|
||||
}[load_settings.dtype]
|
||||
checkpoint_path = _resolve_local_checkpoint(save_dir)
|
||||
|
||||
if bnb_available:
|
||||
log_lines.append("\nModel too large for chat at float16 — reloading in 4-bit...")
|
||||
log_lines.append(
|
||||
f"\nModel too large for chat at {dtype_label} — reloading in 4-bit..."
|
||||
)
|
||||
last_yielded[0] = len(log_lines)
|
||||
yield status_msg, "\n".join(log_lines), gr.update(), gr.update(), gr.update(), gr.update()
|
||||
try:
|
||||
@@ -2357,17 +2490,11 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
bnb_4bit_quant_type="nf4",
|
||||
llm_int8_enable_fp32_cpu_offload=True,
|
||||
)
|
||||
model_reloaded = _load_model_to_device(
|
||||
save_dir,
|
||||
quantization_config=bnb_cfg,
|
||||
model_reloaded, tokenizer_reloaded = _reload_local_checkpoint(
|
||||
checkpoint_path,
|
||||
trust_remote_code=True,
|
||||
model_kwargs={"quantization_config": bnb_cfg},
|
||||
)
|
||||
tokenizer_reloaded = AutoTokenizer.from_pretrained(
|
||||
save_dir,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
if tokenizer_reloaded.pad_token is None:
|
||||
tokenizer_reloaded.pad_token = tokenizer_reloaded.eos_token
|
||||
|
||||
# Re-install activation steering hooks on the reloaded model
|
||||
if steering_meta:
|
||||
@@ -2382,14 +2509,17 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
can_generate = True
|
||||
log_lines.append("Reloaded in 4-bit — chat is ready!")
|
||||
except Exception as e:
|
||||
log_lines.append(f"4-bit reload failed: {e}")
|
||||
log_lines.append(
|
||||
f"4-bit reload failed: {_format_checkpoint_reload_error(e)}"
|
||||
)
|
||||
_clear_gpu()
|
||||
|
||||
# -- Attempt 2: CPU offloading (slower but no extra dependencies)
|
||||
if not can_generate:
|
||||
import tempfile
|
||||
log_lines.append(
|
||||
"\nModel too large for chat at float16 — reloading with CPU offload..."
|
||||
f"\nModel too large for chat at {dtype_label} — "
|
||||
"reloading with CPU offload..."
|
||||
if not bnb_available
|
||||
else "Falling back to CPU offload..."
|
||||
)
|
||||
@@ -2397,22 +2527,18 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
yield status_msg, "\n".join(log_lines), gr.update(), gr.update(), gr.update(), gr.update()
|
||||
try:
|
||||
offload_dir = tempfile.mkdtemp(prefix="obliteratus_offload_")
|
||||
model_reloaded = _load_model_to_device(
|
||||
save_dir,
|
||||
offload_folder=offload_dir,
|
||||
torch_dtype={
|
||||
"float16": torch.float16,
|
||||
"bfloat16": torch.bfloat16,
|
||||
"float32": torch.float32,
|
||||
}[load_settings.dtype],
|
||||
model_reloaded, tokenizer_reloaded = _reload_local_checkpoint(
|
||||
checkpoint_path,
|
||||
trust_remote_code=True,
|
||||
model_kwargs={
|
||||
"offload_folder": offload_dir,
|
||||
"torch_dtype": {
|
||||
"float16": torch.float16,
|
||||
"bfloat16": torch.bfloat16,
|
||||
"float32": torch.float32,
|
||||
}[load_settings.dtype],
|
||||
},
|
||||
)
|
||||
tokenizer_reloaded = AutoTokenizer.from_pretrained(
|
||||
save_dir,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
if tokenizer_reloaded.pad_token is None:
|
||||
tokenizer_reloaded.pad_token = tokenizer_reloaded.eos_token
|
||||
|
||||
# Re-install activation steering hooks on the reloaded model
|
||||
if steering_meta:
|
||||
@@ -2427,8 +2553,13 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
can_generate = True
|
||||
log_lines.append("Reloaded with CPU offload — chat is ready (may be slower).")
|
||||
except Exception as e:
|
||||
log_lines.append(f"CPU offload reload failed: {e}")
|
||||
log_lines.append("Chat unavailable. Load the saved model on a larger instance.")
|
||||
log_lines.append(
|
||||
f"CPU offload reload failed: {_format_checkpoint_reload_error(e)}"
|
||||
)
|
||||
log_lines.append(
|
||||
"Chat unavailable. The completed checkpoint remains saved; "
|
||||
"review the reload error above."
|
||||
)
|
||||
with _lock:
|
||||
_state["status"] = "idle"
|
||||
|
||||
@@ -2474,6 +2605,7 @@ def obliterate(model_choice: str, method_choice: str,
|
||||
|
||||
except Exception as e:
|
||||
# Ensure status never gets stuck on "obliterating"
|
||||
_cleanup_failed_pipeline(pipeline_ref[0], save_dir)
|
||||
with _lock:
|
||||
_state["status"] = "idle"
|
||||
err_msg = str(e) or repr(e)
|
||||
@@ -2564,15 +2696,17 @@ def chat_respond(message: str, history: list[dict], system_prompt: str,
|
||||
checkpoint = _state.get("output_dir")
|
||||
if _checkpoint_is_available(checkpoint):
|
||||
try:
|
||||
checkpoint = _resolve_local_checkpoint(checkpoint)
|
||||
is_preset = (_state.get("model_name") or "") in MODELS
|
||||
load_settings = _settings_from_metadata(_state.get("load_settings"))
|
||||
model = _load_model_to_device(
|
||||
checkpoint,
|
||||
trust_remote_code=is_preset,
|
||||
local_files_only=True,
|
||||
**_checkpoint_load_kwargs(load_settings),
|
||||
)
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
checkpoint, trust_remote_code=is_preset,
|
||||
checkpoint, trust_remote_code=is_preset, local_files_only=True,
|
||||
)
|
||||
if tokenizer.pad_token is None:
|
||||
tokenizer.pad_token = tokenizer.eos_token
|
||||
@@ -2584,8 +2718,8 @@ def chat_respond(message: str, history: list[dict], system_prompt: str,
|
||||
_state["model"] = model
|
||||
_state["tokenizer"] = tokenizer
|
||||
_state["status"] = "ready"
|
||||
except Exception:
|
||||
yield "Model failed to reload from checkpoint. Try re-obliterating."
|
||||
except Exception as error:
|
||||
yield _format_checkpoint_reload_error(error)
|
||||
return
|
||||
else:
|
||||
_clear_stale_model_state()
|
||||
@@ -2778,14 +2912,16 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
||||
if checkpoint and Path(checkpoint).exists():
|
||||
is_preset = (_state.get("model_name") or "") in MODELS
|
||||
try:
|
||||
checkpoint = _resolve_local_checkpoint(checkpoint)
|
||||
load_settings = _settings_from_metadata(_state.get("load_settings"))
|
||||
model_loaded = _load_model_to_device(
|
||||
checkpoint,
|
||||
trust_remote_code=is_preset,
|
||||
local_files_only=True,
|
||||
**_checkpoint_load_kwargs(load_settings),
|
||||
)
|
||||
tokenizer_loaded = AutoTokenizer.from_pretrained(
|
||||
checkpoint, trust_remote_code=is_preset,
|
||||
checkpoint, trust_remote_code=is_preset, local_files_only=True,
|
||||
)
|
||||
if tokenizer_loaded.pad_token is None:
|
||||
tokenizer_loaded.pad_token = tokenizer_loaded.eos_token
|
||||
@@ -2799,7 +2935,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
yield f"**Error:** Could not reload model: {e}", get_chat_header()
|
||||
yield f"**Error:** {_format_checkpoint_reload_error(e)}", get_chat_header()
|
||||
return
|
||||
yield (
|
||||
"**Error:** Model checkpoint not found. The Space may have restarted — "
|
||||
@@ -2847,13 +2983,15 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
||||
|
||||
is_preset = cfg["model_choice"] in MODELS
|
||||
try:
|
||||
checkpoint_dir = _resolve_local_checkpoint(checkpoint_dir)
|
||||
model_loaded = _load_model_to_device(
|
||||
checkpoint_dir,
|
||||
trust_remote_code=is_preset,
|
||||
local_files_only=True,
|
||||
**_checkpoint_load_kwargs(load_settings),
|
||||
)
|
||||
tokenizer_loaded = AutoTokenizer.from_pretrained(
|
||||
checkpoint_dir, trust_remote_code=is_preset,
|
||||
checkpoint_dir, trust_remote_code=is_preset, local_files_only=True,
|
||||
)
|
||||
if tokenizer_loaded.pad_token is None:
|
||||
tokenizer_loaded.pad_token = tokenizer_loaded.eos_token
|
||||
@@ -2896,9 +3034,10 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
|
||||
checkpoint_dir,
|
||||
quantization_config=bnb_cfg,
|
||||
trust_remote_code=is_preset,
|
||||
local_files_only=True,
|
||||
)
|
||||
tokenizer_loaded = AutoTokenizer.from_pretrained(
|
||||
checkpoint_dir, trust_remote_code=is_preset,
|
||||
checkpoint_dir, trust_remote_code=is_preset, local_files_only=True,
|
||||
)
|
||||
if tokenizer_loaded.pad_token is None:
|
||||
tokenizer_loaded.pad_token = tokenizer_loaded.eos_token
|
||||
@@ -3035,15 +3174,17 @@ def ab_chat_respond(message: str, history_left: list[dict], history_right: list[
|
||||
model_name = _state.get("model_name") or model_name
|
||||
if checkpoint and Path(checkpoint).exists():
|
||||
try:
|
||||
checkpoint = _resolve_local_checkpoint(checkpoint)
|
||||
is_preset = (model_name or "") in MODELS
|
||||
load_settings = _settings_from_metadata(_state.get("load_settings"))
|
||||
abliterated_model = _load_model_to_device(
|
||||
checkpoint,
|
||||
trust_remote_code=is_preset,
|
||||
local_files_only=True,
|
||||
**_checkpoint_load_kwargs(load_settings),
|
||||
)
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
checkpoint, trust_remote_code=is_preset,
|
||||
checkpoint, trust_remote_code=is_preset, local_files_only=True,
|
||||
)
|
||||
if tokenizer.pad_token is None:
|
||||
tokenizer.pad_token = tokenizer.eos_token
|
||||
|
||||
Reference in New Issue
Block a user