feat: make UI model loading configurable

This commit is contained in:
Joseph Magly
2026-08-24 11:53:18 -04:00
parent 2b7328242c
commit 8c4c68760d
8 changed files with 503 additions and 31 deletions
+9
View File
@@ -540,6 +540,15 @@ This sets `CUDA_VISIBLE_DEVICES` before CUDA initializes. The model is then shar
The `--dtype` flag controls the precision of model weights, which directly determines how much VRAM you need. Lower precision means smaller memory footprint at the cost of some numerical fidelity:
The web UI exposes the same choice as **Quantization** and **Compute dtype** controls on Obliterate, Benchmark, Multi-Model, and Strength Sweep runs. `Auto (default)` preserves the historical behavior: FP16 compute plus automatic 4-bit loading when the memory estimate or known-model fallback says it is needed. Choose **None + BF16** (or FP16) when the native checkpoint fits in available VRAM and you are modifying weights; native floating-point weights generally provide better numerical fidelity for surgery. Quantized loading remains the right choice when fitting the model is the primary constraint. The resolved mode is printed before work starts and stored with results so it can be checked after the run.
CLI users can make the equivalent explicit selection with, for example:
```bash
obliteratus obliterate Qwen/Qwen3.8-27B --dtype bfloat16
obliteratus obliterate Qwen/Qwen3.8-27B --dtype float16 --quantization 4bit
```
Install the optional backend before selecting a bitsandbytes mode:
```bash
+249 -31
View File
@@ -60,6 +60,12 @@ import torch
from obliteratus import device as dev
from obliteratus.credential_sources import resolve_first, resolve_secret, secret_available
from obliteratus.ui_vram import render_vram_html
from obliteratus.model_load_settings import (
DTYPE_CHOICES,
QUANTIZATION_CHOICES,
ModelLoadSettings,
resolve_model_load_settings,
)
from transformers import AutoModelForCausalLM, AutoTokenizer, TextIteratorStreamer
# ── ZeroGPU support ─────────────────────────────────────────────────
@@ -145,6 +151,41 @@ def _load_model_to_device(
return model
def _checkpoint_load_kwargs(load_settings: ModelLoadSettings) -> dict:
"""Translate resolved settings for direct loading of a saved checkpoint."""
torch_dtype = {
"float16": torch.float16,
"bfloat16": torch.bfloat16,
"float32": torch.float32,
}[load_settings.dtype]
kwargs = {"torch_dtype": torch_dtype}
if load_settings.quantization:
from transformers import BitsAndBytesConfig
if load_settings.quantization == "4bit":
kwargs["quantization_config"] = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_compute_dtype=torch_dtype,
bnb_4bit_quant_type="nf4",
llm_int8_enable_fp32_cpu_offload=True,
)
else:
kwargs["quantization_config"] = BitsAndBytesConfig(
load_in_8bit=True,
llm_int8_enable_fp32_cpu_offload=True,
)
return kwargs
def _settings_from_metadata(metadata: dict | None) -> ModelLoadSettings:
"""Restore concrete settings without re-running model auto-detection."""
saved = metadata or {}
return resolve_model_load_settings(
saved.get("requested_quantization", "Auto (default)"),
saved.get("requested_dtype", "Auto (default)"),
auto_quantization=lambda _dtype: saved.get("quantization"),
)
# ---------------------------------------------------------------------------
# Global state
# ---------------------------------------------------------------------------
@@ -161,6 +202,7 @@ _state: dict = {
# Checkpoint directory for ZeroGPU reload (model tensors may become stale
# after GPU deallocation — this path lets chat_respond reload from disk)
"output_dir": None,
"load_settings": {},
}
_lock = threading.Lock()
@@ -233,6 +275,7 @@ def _recover_sessions_from_disk() -> None:
"prompt_volume": data.get("prompt_volume", 0),
"output_dir": str(p),
"source": data.get("source", "recovered"),
"load_settings": data.get("load_settings", {}),
}
found_any = True
# Track the latest for auto-select
@@ -254,6 +297,7 @@ def _recover_sessions_from_disk() -> None:
_state["output_dir"] = _session_models[latest]["output_dir"]
_state["model_name"] = _session_models[latest].get("model_choice")
_state["method"] = _session_models[latest].get("method")
_state["load_settings"] = _session_models[latest].get("load_settings", {})
# Run recovery on import (app startup)
@@ -716,7 +760,11 @@ _NEEDS_QUANTIZATION = {
}
def _should_quantize(model_id: str, is_preset: bool = False) -> str | None:
def _should_quantize(
model_id: str,
is_preset: bool = False,
dtype: str = "float16",
) -> str | None:
"""Return '4bit' if the model needs quantization for available GPU, else None."""
try:
from obliteratus.models.loader import _estimate_model_memory_gb, _available_gpu_memory_gb
@@ -726,7 +774,12 @@ def _should_quantize(model_id: str, is_preset: bool = False) -> str | None:
# Skip if model already ships with native quantization (e.g. Mxfp4Config)
if getattr(config, "quantization_config", None) is not None:
return None
est_gb = _estimate_model_memory_gb(config, torch.float16)
torch_dtype = {
"float16": torch.float16,
"bfloat16": torch.bfloat16,
"float32": torch.float32,
}[dtype]
est_gb = _estimate_model_memory_gb(config, torch_dtype)
gpu_gb = _available_gpu_memory_gb()
if gpu_gb > 0 and est_gb > gpu_gb * 0.85:
return "4bit"
@@ -738,6 +791,36 @@ def _should_quantize(model_id: str, is_preset: bool = False) -> str | None:
return None
def _resolve_ui_load_settings(
model_id: str,
is_preset: bool,
quantization_choice: str | None,
dtype_choice: str | None,
) -> ModelLoadSettings:
"""Resolve UI controls while retaining the historical Auto behavior."""
settings = resolve_model_load_settings(
quantization_choice,
dtype_choice,
auto_quantization=lambda dtype: _should_quantize(
model_id, is_preset=is_preset, dtype=dtype,
),
)
target = dev.get_device("auto")
if settings.requested_dtype == "BF16" and not dev.supports_bfloat16(target):
raise ValueError(
f"BF16 is not supported on {target!r}. Choose FP16 or FP32.",
)
if (
settings.requested_quantization in {"4-bit", "8-bit"}
and not dev.supports_bitsandbytes(target)
):
raise ValueError(
f"{settings.requested_quantization} loading requires an NVIDIA CUDA GPU "
"and the optional bitsandbytes dependency.",
)
return settings
# ---------------------------------------------------------------------------
# Obliteration
# ---------------------------------------------------------------------------
@@ -771,6 +854,7 @@ def _clear_stale_model_state() -> None:
"status": "idle",
"steering": None,
"output_dir": None,
"load_settings": {},
})
@@ -991,7 +1075,12 @@ _BENCH_TEST_PROMPTS = [
]
def _format_obliteration_metrics(pipeline, method: str, elapsed_str: str) -> str:
def _format_obliteration_metrics(
pipeline,
method: str,
elapsed_str: str,
load_settings: ModelLoadSettings | None = None,
) -> str:
"""Format post-obliteration quality metrics as a concise Markdown card."""
metrics = getattr(pipeline, "_quality_metrics", {}) or {}
ppl = metrics.get("perplexity")
@@ -1020,6 +1109,9 @@ def _format_obliteration_metrics(pipeline, method: str, elapsed_str: str) -> str
parts.append(f"| KL Divergence | **{kl:.4f}** | {icon} |")
if n_layers > 0:
parts.append(f"| Layers Modified | **{n_layers}** | |")
if load_settings is not None:
parts.append(f"| Quantization | **{load_settings.quantization or 'none'}** | |")
parts.append(f"| Compute dtype | **{load_settings.dtype}** | |")
if not metrics:
return ""
@@ -1132,6 +1224,8 @@ def benchmark(
methods_to_test: list[str],
prompt_volume_choice: str,
dataset_source_choice: str = "",
quantization_choice: str = "Auto (default)",
dtype_choice: str = "Auto (default)",
progress=gr.Progress(),
):
"""Run multiple abliteration methods on a single model and compare results.
@@ -1148,6 +1242,13 @@ def benchmark(
is_preset = model_choice in MODELS
prompt_volume = PROMPT_VOLUMES.get(prompt_volume_choice, 33)
dataset_key = get_source_key_from_label(dataset_source_choice) if dataset_source_choice else "builtin"
try:
load_settings = _resolve_ui_load_settings(
model_id, is_preset, quantization_choice, dtype_choice,
)
except ValueError as exc:
yield f"**Error:** {exc}", "", str(exc), None
return
if not methods_to_test:
methods_to_test = ["basic", "advanced", "surgical"]
@@ -1184,6 +1285,7 @@ def benchmark(
all_logs.append(f"Methods: {', '.join(methods_to_test)}")
all_logs.append(f"Dataset: {source_label} ({len(harmful_all)} prompts available)")
all_logs.append(f"Prompt volume: {vol_label} (using {actual_n} pairs)")
all_logs.append(f"Resolved load mode: {load_settings.summary}")
all_logs.append("=" * 60)
yield "**Starting benchmark...**", "", "\n".join(all_logs), None
@@ -1220,7 +1322,7 @@ def benchmark(
if result.status == "running":
run_logs.append(f"{stage_key.upper()}{result.message}")
quantization = _should_quantize(model_id, is_preset=is_preset)
quantization = load_settings.quantization
def run_pipeline():
try:
@@ -1235,7 +1337,7 @@ def benchmark(
model_name=model_id,
output_dir=f"/tmp/bench_{method_key}",
device="auto",
dtype="float16",
dtype=load_settings.dtype,
quantization=quantization,
trust_remote_code=is_preset,
harmful_prompts=harmful_all[:n],
@@ -1251,7 +1353,7 @@ def benchmark(
model_name=model_id,
output_dir=f"/tmp/bench_{method_key}",
device="auto",
dtype="float16",
dtype=load_settings.dtype,
method=method_key,
quantization=quantization,
trust_remote_code=is_preset,
@@ -1291,6 +1393,8 @@ def benchmark(
"model": model_id,
"time_s": round(elapsed, 1),
"error": None,
"quantization": quantization,
"dtype": load_settings.dtype,
}
if run_error is not None:
@@ -1343,6 +1447,7 @@ def benchmark(
dataset=source_label,
n_prompts=actual_n,
quantization=quantization,
compute_dtype=load_settings.dtype,
)
except Exception:
pass # Telemetry is best-effort, never block benchmarks
@@ -1358,6 +1463,7 @@ def benchmark(
"method": method_key,
"dataset_key": dataset_key,
"prompt_volume": prompt_volume,
"load_settings": load_settings.metadata(),
"output_dir": bench_save_path,
}
_persist_session_meta(bench_save_path, label, {
@@ -1366,6 +1472,7 @@ def benchmark(
"method": method_key,
"dataset_key": dataset_key,
"prompt_volume": prompt_volume,
"load_settings": load_settings.metadata(),
"source": "benchmark",
})
@@ -1492,6 +1599,8 @@ def benchmark_multi_model(
method_choice: str,
prompt_volume_choice: str,
dataset_source_choice: str = "",
quantization_choice: str = "Auto (default)",
dtype_choice: str = "Auto (default)",
progress=gr.Progress(),
):
"""Run one abliteration method across multiple models and compare.
@@ -1578,7 +1687,16 @@ def benchmark_multi_model(
def on_stage(result):
pass
quantization = _should_quantize(model_id, is_preset=is_preset_model)
try:
load_settings = _resolve_ui_load_settings(
model_id, is_preset_model, quantization_choice, dtype_choice,
)
except ValueError as exc:
all_logs.append(f" ERROR: {exc}")
yield f"**Error:** {exc}", _format_multi_model_results(results, bench_context), "\n".join(all_logs), None
return
quantization = load_settings.quantization
all_logs.append(f"Resolved load mode: {load_settings.summary}")
def run_pipeline():
try:
@@ -1590,7 +1708,7 @@ def benchmark_multi_model(
model_name=model_id,
output_dir=f"/tmp/bench_mm_{mi}",
device="auto",
dtype="float16",
dtype=load_settings.dtype,
quantization=quantization,
trust_remote_code=is_preset_model,
harmful_prompts=harmful_all[:n],
@@ -1606,7 +1724,7 @@ def benchmark_multi_model(
model_name=model_id,
output_dir=f"/tmp/bench_mm_{mi}",
device="auto",
dtype="float16",
dtype=load_settings.dtype,
method=method_key,
quantization=quantization,
trust_remote_code=is_preset_model,
@@ -1645,6 +1763,8 @@ def benchmark_multi_model(
"method": method_key,
"time_s": round(elapsed, 1),
"error": None,
"quantization": quantization,
"dtype": load_settings.dtype,
}
if run_error is not None:
@@ -1695,6 +1815,7 @@ def benchmark_multi_model(
dataset=source_label,
n_prompts=actual_n,
quantization=quantization,
compute_dtype=load_settings.dtype,
)
except Exception:
pass # Telemetry is best-effort
@@ -1710,6 +1831,7 @@ def benchmark_multi_model(
"method": method_key,
"dataset_key": dataset_key,
"prompt_volume": prompt_volume,
"load_settings": load_settings.metadata(),
"output_dir": mm_save_path,
}
_persist_session_meta(mm_save_path, label, {
@@ -1718,6 +1840,7 @@ def benchmark_multi_model(
"method": method_key,
"dataset_key": dataset_key,
"prompt_volume": prompt_volume,
"load_settings": load_settings.metadata(),
"source": "benchmark_mm",
})
@@ -1832,6 +1955,7 @@ def _format_multi_model_results(results: list[dict], context: dict | None = None
def obliterate(model_choice: str, method_choice: str,
prompt_volume_choice: str, dataset_source_choice: str,
custom_harmful: str, custom_harmless: str,
quantization_choice: str, dtype_choice: str,
# Advanced params (sliders + radio)
adv_n_directions: int, adv_direction_method: str,
adv_regularization: float,
@@ -1867,6 +1991,13 @@ def obliterate(model_choice: str, method_choice: str,
is_preset = model_choice in MODELS
method = METHODS.get(method_choice, "advanced")
prompt_volume = PROMPT_VOLUMES.get(prompt_volume_choice, 33)
try:
load_settings = _resolve_ui_load_settings(
model_id, is_preset, quantization_choice, dtype_choice,
)
except ValueError as exc:
yield f"**Error:** {exc}", str(exc), get_chat_header(), gr.update(), gr.update(), gr.update()
return
# Resolve "adaptive" → telemetry-recommended method for this model
_adaptive_info = ""
@@ -1956,7 +2087,7 @@ def obliterate(model_choice: str, method_choice: str,
idx = stage_order.get(stage_key, 0)
progress((idx + 1) / 6, desc=f"{stage_key.upper()}")
quantization = _should_quantize(model_id, is_preset=is_preset)
quantization = load_settings.quantization
def run_pipeline():
try:
@@ -1985,7 +2116,7 @@ def obliterate(model_choice: str, method_choice: str,
model_name=model_id,
output_dir=save_dir,
device="auto",
dtype="float16",
dtype=load_settings.dtype,
quantization=quantization,
trust_remote_code=is_preset,
harmful_prompts=harmful_all[:n],
@@ -2001,7 +2132,7 @@ def obliterate(model_choice: str, method_choice: str,
model_name=model_id,
output_dir=save_dir,
device="auto",
dtype="float16",
dtype=load_settings.dtype,
method=method,
quantization=quantization,
trust_remote_code=is_preset,
@@ -2065,8 +2196,7 @@ def obliterate(model_choice: str, method_choice: str,
log_lines.append(f"Dataset: {source_label}")
vol_label = "all" if prompt_volume == -1 else str(prompt_volume)
log_lines.append(f"Prompt volume: {vol_label} pairs")
if quantization:
log_lines.append(f"Quantization: {quantization} (auto-detected for GPU fit)")
log_lines.append(f"Resolved load mode: {load_settings.summary}")
log_lines.append("")
worker = threading.Thread(target=run_pipeline, daemon=True)
@@ -2135,6 +2265,7 @@ def obliterate(model_choice: str, method_choice: str,
dataset=ds_label,
n_prompts=prompt_volume,
quantization=quantization,
compute_dtype=load_settings.dtype,
)
maybe_send_pipeline_report(pipeline)
except Exception:
@@ -2168,9 +2299,11 @@ def obliterate(model_choice: str, method_choice: str,
"prompt_volume": prompt_volume,
"output_dir": save_dir,
"source": "obliterate",
"load_settings": load_settings.metadata(),
}
_state["steering"] = steering_meta
_state["output_dir"] = save_dir # for ZeroGPU checkpoint reload
_state["load_settings"] = load_settings.metadata()
# Persist session metadata to disk so we survive ZeroGPU process restarts
_persist_session_meta(save_dir, _cache_label, {
@@ -2180,6 +2313,7 @@ def obliterate(model_choice: str, method_choice: str,
"dataset_key": dataset_key if not use_custom else "custom",
"prompt_volume": prompt_volume,
"source": "obliterate",
"load_settings": load_settings.metadata(),
})
if can_generate:
@@ -2261,7 +2395,11 @@ def obliterate(model_choice: str, method_choice: str,
model_reloaded = _load_model_to_device(
save_dir,
offload_folder=offload_dir,
torch_dtype=torch.float16,
torch_dtype={
"float16": torch.float16,
"bfloat16": torch.bfloat16,
"float32": torch.float32,
}[load_settings.dtype],
trust_remote_code=True,
)
tokenizer_reloaded = AutoTokenizer.from_pretrained(
@@ -2290,7 +2428,9 @@ def obliterate(model_choice: str, method_choice: str,
_state["status"] = "idle"
# Build metrics summary card while pipeline is still alive
metrics_card = _format_obliteration_metrics(pipeline, method, _elapsed())
metrics_card = _format_obliteration_metrics(
pipeline, method, _elapsed(), load_settings,
)
# Free pipeline internals we no longer need (activations, directions cache)
# to reclaim memory — we've already extracted the model and steering metadata.
@@ -2420,9 +2560,11 @@ def chat_respond(message: str, history: list[dict], system_prompt: str,
if _checkpoint_is_available(checkpoint):
try:
is_preset = (_state.get("model_name") or "") in MODELS
load_settings = _settings_from_metadata(_state.get("load_settings"))
model = _load_model_to_device(
checkpoint, torch_dtype=torch.float16,
checkpoint,
trust_remote_code=is_preset,
**_checkpoint_load_kwargs(load_settings),
)
tokenizer = AutoTokenizer.from_pretrained(
checkpoint, trust_remote_code=is_preset,
@@ -2631,9 +2773,11 @@ 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:
load_settings = _settings_from_metadata(_state.get("load_settings"))
model_loaded = _load_model_to_device(
checkpoint, torch_dtype=torch.float16,
checkpoint,
trust_remote_code=is_preset,
**_checkpoint_load_kwargs(load_settings),
)
tokenizer_loaded = AutoTokenizer.from_pretrained(
checkpoint, trust_remote_code=is_preset,
@@ -2663,6 +2807,11 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
model_id = cfg["model_id"]
method_key = cfg["method"]
checkpoint_dir = cfg.get("output_dir")
try:
load_settings = _settings_from_metadata(cfg.get("load_settings"))
except ValueError as exc:
yield f"**Error:** Saved model load settings are invalid: {exc}", get_chat_header()
return
# If this model is already the active one, skip the destructive reload
with _lock:
@@ -2683,6 +2832,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
_state["status"] = "obliterating"
_state["model_name"] = cfg["model_choice"]
_state["method"] = method_key
_state["load_settings"] = load_settings.metadata()
_clear_gpu()
# If we have a saved checkpoint on disk, load directly — no re-training!
@@ -2694,8 +2844,8 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
try:
model_loaded = _load_model_to_device(
checkpoint_dir,
torch_dtype=torch.float16,
trust_remote_code=is_preset,
**_checkpoint_load_kwargs(load_settings),
)
tokenizer_loaded = AutoTokenizer.from_pretrained(
checkpoint_dir, trust_remote_code=is_preset,
@@ -2715,6 +2865,16 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
)
return
except Exception:
if load_settings.requested_quantization != "Auto (default)":
_clear_gpu()
with _lock:
_state["status"] = "idle"
yield (
f"**Error:** Could not load `{choice}` with the requested "
f"settings ({load_settings.summary}).",
get_chat_header(),
)
return
# Checkpoint load failed (e.g. GPU too small at fp16) — try 4-bit
_clear_gpu()
try:
@@ -2771,7 +2931,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
n = min(len(harmful_all), len(harmless_all))
is_preset = cfg["model_choice"] in MODELS
quantization = _should_quantize(model_id, is_preset=is_preset)
quantization = load_settings.quantization
pipeline_ref = [None]
error_ref = [None]
@@ -2783,7 +2943,7 @@ def load_bench_into_chat(choice: str, progress=gr.Progress()):
model_name=model_id,
output_dir="/tmp/obliterated",
device="auto",
dtype="float16",
dtype=load_settings.dtype,
method=method_key,
quantization=quantization,
trust_remote_code=is_preset,
@@ -2871,9 +3031,11 @@ def ab_chat_respond(message: str, history_left: list[dict], history_right: list[
if checkpoint and Path(checkpoint).exists():
try:
is_preset = (model_name or "") in MODELS
load_settings = _settings_from_metadata(_state.get("load_settings"))
abliterated_model = _load_model_to_device(
checkpoint, torch_dtype=torch.float16,
checkpoint,
trust_remote_code=is_preset,
**_checkpoint_load_kwargs(load_settings),
)
tokenizer = AutoTokenizer.from_pretrained(
checkpoint, trust_remote_code=is_preset,
@@ -3002,10 +3164,12 @@ def ab_chat_respond(message: str, history_left: list[dict], history_right: list[
is_preset = model_name in MODELS
original_response = ""
try:
load_settings = _settings_from_metadata(_state.get("load_settings"))
original_model = _load_model_to_device(
model_id, torch_dtype=torch.float16,
model_id,
trust_remote_code=is_preset,
low_cpu_mem_usage=True,
**_checkpoint_load_kwargs(load_settings),
token=resolve_secret("HF_TOKEN"),
)
@@ -3074,7 +3238,10 @@ def ab_chat_respond(message: str, history_left: list[dict], history_right: list[
@spaces.GPU(duration=300)
def strength_sweep(model_choice: str, method_choice: str,
prompt_vol_choice: str, dataset_source_choice: str,
sweep_steps: int, progress=gr.Progress()):
sweep_steps: int,
quantization_choice: str = "Auto (default)",
dtype_choice: str = "Auto (default)",
progress=gr.Progress()):
"""Sweep regularization from 0.0→1.0 and measure refusal rate + perplexity.
Produces a dose-response curve: the fundamental plot for abliteration research.
@@ -3086,13 +3253,21 @@ def strength_sweep(model_choice: str, method_choice: str,
is_preset = model_choice in MODELS
method_key = METHODS.get(method_choice, "advanced")
dataset_key = get_source_key_from_label(dataset_source_choice) if dataset_source_choice else "builtin"
try:
load_settings = _resolve_ui_load_settings(
model_id, is_preset, quantization_choice, dtype_choice,
)
except ValueError as exc:
yield f"Error: {exc}", "", str(exc), None, None
return
sweep_steps = max(3, min(int(sweep_steps), 20))
regs = [round(i / (sweep_steps - 1), 3) for i in range(sweep_steps)]
results = []
all_logs = [f"Ablation Strength Sweep: {model_choice} x {method_key}",
f"Sweep points: {regs}", ""]
f"Sweep points: {regs}",
f"Resolved load mode: {load_settings.summary}", ""]
yield "Starting sweep...", "", "\n".join(all_logs), None, None
@@ -3121,12 +3296,12 @@ def strength_sweep(model_choice: str, method_choice: str,
def _run_sweep_point():
try:
quantization = _should_quantize(model_id, is_preset=is_preset)
quantization = load_settings.quantization
pipe = AbliterationPipeline(
model_id, method=method_key,
output_dir=f"/tmp/sweep_{step_i}",
device="auto",
dtype="float16",
dtype=load_settings.dtype,
quantization=quantization,
trust_remote_code=is_preset,
harmful_prompts=harmful, harmless_prompts=harmless,
@@ -4076,6 +4251,18 @@ with gr.Blocks(title="OBLITERATUS", fill_height=True) as demo:
label="Dataset Source",
info="Built-in (512 pairs) or download larger research datasets from HuggingFace",
)
quantization_dd = gr.Dropdown(
choices=list(QUANTIZATION_CHOICES),
value="Auto (default)",
label="Quantization",
info="Auto preserves memory-based 4-bit selection. Choose None for native weights.",
)
dtype_dd = gr.Dropdown(
choices=list(DTYPE_CHOICES),
value="Auto (default)",
label="Compute dtype",
info="Auto preserves the existing FP16 behavior. BF16 is recommended for weight surgery when supported.",
)
dataset_info_md = gr.Markdown(
f"*{DATASET_SOURCES['builtin'].description}*",
elem_classes=["dataset-info"],
@@ -4330,6 +4517,15 @@ result = client.predict(
label="Dataset Source",
info="Select prompt dataset for benchmarking",
)
with gr.Row():
bench_quantization = gr.Dropdown(
choices=list(QUANTIZATION_CHOICES), value="Auto (default)",
label="Quantization",
)
bench_dtype = gr.Dropdown(
choices=list(DTYPE_CHOICES), value="Auto (default)",
label="Compute dtype",
)
bench_btn = gr.Button(
"Run Multi-Method Benchmark",
variant="primary", size="lg",
@@ -4434,6 +4630,15 @@ result = client.predict(
value=get_source_choices()[0],
label="Dataset Source",
)
with gr.Row():
mm_quantization = gr.Dropdown(
choices=list(QUANTIZATION_CHOICES), value="Auto (default)",
label="Quantization",
)
mm_dtype = gr.Dropdown(
choices=list(DTYPE_CHOICES), value="Auto (default)",
label="Compute dtype",
)
mm_btn = gr.Button(
"Run Multi-Model Benchmark",
variant="primary", size="lg",
@@ -4764,6 +4969,15 @@ tradeoff point where refusal is minimized with minimal capability damage.
label="Sweep Points",
info="Number of regularization values to test (more = finer curve, slower)",
)
with gr.Row():
sweep_quantization = gr.Dropdown(
choices=list(QUANTIZATION_CHOICES), value="Auto (default)",
label="Quantization",
)
sweep_dtype = gr.Dropdown(
choices=list(DTYPE_CHOICES), value="Auto (default)",
label="Compute dtype",
)
sweep_btn = gr.Button("Run Sweep", variant="primary")
sweep_status = gr.Markdown("")
@@ -4781,7 +4995,8 @@ tradeoff point where refusal is minimized with minimal capability damage.
sweep_btn.click(
fn=strength_sweep,
inputs=[sweep_model_dd, sweep_method_dd, sweep_vol_dd,
sweep_dataset_dd, sweep_steps_slider],
sweep_dataset_dd, sweep_steps_slider,
sweep_quantization, sweep_dtype],
outputs=[sweep_status, sweep_results, sweep_log, sweep_gallery,
gr.State()], # 5th output is unused File placeholder
)
@@ -5194,7 +5409,8 @@ Built on the shoulders of:
# Wire benchmark → Chat/A/B cross-tab dropdown updates
bench_btn.click(
fn=benchmark,
inputs=[bench_model, bench_methods, bench_prompt_vol, bench_dataset],
inputs=[bench_model, bench_methods, bench_prompt_vol, bench_dataset,
bench_quantization, bench_dtype],
outputs=[bench_status, bench_results, bench_log, bench_gallery],
api_name="/benchmark",
).then(
@@ -5214,7 +5430,8 @@ Built on the shoulders of:
mm_btn.click(
fn=benchmark_multi_model,
inputs=[mm_models, mm_method, mm_prompt_vol, mm_dataset],
inputs=[mm_models, mm_method, mm_prompt_vol, mm_dataset,
mm_quantization, mm_dtype],
outputs=[mm_status, mm_results, mm_log, mm_gallery],
api_name="/benchmark_multi_model",
).then(
@@ -5239,7 +5456,8 @@ Built on the shoulders of:
obliterate_btn.click(
fn=obliterate,
inputs=[model_dd, method_dd, prompt_vol_dd, dataset_dd,
custom_harmful_tb, custom_harmless_tb] + _adv_controls,
custom_harmful_tb, custom_harmless_tb,
quantization_dd, dtype_dd] + _adv_controls,
outputs=[status_md, log_box, chat_status, session_model_dd, metrics_md, ab_session_model_dd],
).then(
fn=lambda: _get_vram_html(),
+3
View File
@@ -22,6 +22,7 @@
],
"paths": [
"app.py",
"obliteratus/model_load_settings.py",
"obliteratus/__init__.py",
"obliteratus/__main__.py",
"obliteratus/analysis/__init__.py",
@@ -33,6 +34,8 @@
],
"required_tests": [
"tests/test_app_vram.py",
"tests/test_app_model_load_settings.py",
"tests/test_model_load_settings.py",
"tests/test_cli.py",
"tests/test_cli_boundaries.py",
"tests/test_module_imports.py",
+83
View File
@@ -0,0 +1,83 @@
"""Resolve operator-facing model load choices into loader arguments."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Callable
QUANTIZATION_CHOICES = ("Auto (default)", "None", "4-bit", "8-bit")
DTYPE_CHOICES = ("Auto (default)", "BF16", "FP16", "FP32")
_QUANTIZATION_VALUES = {
"Auto (default)": "auto",
"auto": "auto",
"None": None,
"none": None,
"4-bit": "4bit",
"4bit": "4bit",
"8-bit": "8bit",
"8bit": "8bit",
}
_DTYPE_VALUES = {
"Auto (default)": "float16",
"auto": "float16",
"BF16": "bfloat16",
"bfloat16": "bfloat16",
"FP16": "float16",
"float16": "float16",
"FP32": "float32",
"float32": "float32",
}
@dataclass(frozen=True)
class ModelLoadSettings:
"""Requested UI values and the concrete values sent to the loader."""
requested_quantization: str
requested_dtype: str
quantization: str | None
dtype: str
@property
def summary(self) -> str:
quantization = self.quantization or "none"
return f"quantization={quantization}, compute dtype={self.dtype}"
def metadata(self) -> dict[str, str | None]:
return asdict(self)
def resolve_model_load_settings(
quantization_choice: str | None,
dtype_choice: str | None,
*,
auto_quantization: Callable[[str], str | None],
) -> ModelLoadSettings:
"""Validate UI/API choices and return concrete pipeline load settings.
``Auto`` intentionally resolves dtype to ``float16`` because that was the
web application's historical hard-coded value. Quantization Auto delegates
to the existing memory-estimation and fallback policy supplied by the app.
"""
requested_quantization = quantization_choice or "Auto (default)"
requested_dtype = dtype_choice or "Auto (default)"
if requested_quantization not in _QUANTIZATION_VALUES:
choices = ", ".join(QUANTIZATION_CHOICES)
raise ValueError(f"Unsupported quantization choice {requested_quantization!r}. Choose: {choices}.")
if requested_dtype not in _DTYPE_VALUES:
choices = ", ".join(DTYPE_CHOICES)
raise ValueError(f"Unsupported compute dtype {requested_dtype!r}. Choose: {choices}.")
dtype = _DTYPE_VALUES[requested_dtype]
quantization = _QUANTIZATION_VALUES[requested_quantization]
if quantization == "auto":
quantization = auto_quantization(dtype)
return ModelLoadSettings(
requested_quantization=requested_quantization,
requested_dtype=requested_dtype,
quantization=quantization,
dtype=dtype,
)
+3
View File
@@ -361,6 +361,7 @@ class BenchmarkRecord:
gpu_name: str = ""
gpu_vram_gb: float = 0.0
quantization: str | None = None
compute_dtype: str = ""
# Extra metadata
extra: dict[str, Any] = field(default_factory=dict)
@@ -803,6 +804,7 @@ def log_benchmark_from_dict(
dataset: str = "",
n_prompts: int = 0,
quantization: str | None = None,
compute_dtype: str = "",
pipeline_config: dict[str, Any] | None = None,
) -> bool:
"""Convenience wrapper: create a BenchmarkRecord from benchmark result dict.
@@ -817,6 +819,7 @@ def log_benchmark_from_dict(
dataset=dataset,
n_prompts=n_prompts,
quantization=quantization,
compute_dtype=compute_dtype,
refusal_rate=entry.get("refusal_rate"),
perplexity=entry.get("perplexity"),
coherence=entry.get("coherence"),
+60
View File
@@ -0,0 +1,60 @@
"""Web-application contracts for configurable model loading."""
from __future__ import annotations
import subprocess
import sys
import pytest
@pytest.mark.operator_ui
def test_ui_resolves_auto_explicit_bfloat16_and_invalid_hardware():
"""Exercise app helpers in isolation from Gradio's import-time sockets."""
script = r'''
from unittest.mock import Mock
import torch
import app
app.dev.get_device = lambda _requested="auto": "cuda:0"
app.dev.supports_bfloat16 = lambda _device=None: True
app.dev.supports_bitsandbytes = lambda _device=None: True
automatic = Mock(return_value="4bit")
app._should_quantize = automatic
settings = app._resolve_ui_load_settings(
"Qwen/Qwen3.8-27B", True, "Auto (default)", "Auto (default)",
)
assert settings.quantization == "4bit"
assert settings.dtype == "float16"
automatic.assert_called_once_with(
"Qwen/Qwen3.8-27B", is_preset=True, dtype="float16",
)
automatic = Mock(side_effect=AssertionError("automatic policy must not run"))
app._should_quantize = automatic
settings = app._resolve_ui_load_settings(
"Qwen/Qwen3.8-27B", True, "None", "BF16",
)
assert settings.quantization is None
assert settings.dtype == "bfloat16"
assert app._checkpoint_load_kwargs(settings) == {"torch_dtype": torch.bfloat16}
automatic.assert_not_called()
app.dev.supports_bfloat16 = lambda _device=None: False
try:
app._resolve_ui_load_settings("org/model", False, "None", "BF16")
except ValueError as exc:
assert "BF16 is not supported" in str(exc)
assert "FP16 or FP32" in str(exc)
else:
raise AssertionError("unsupported BF16 must fail before pipeline construction")
'''
result = subprocess.run(
[sys.executable, "-c", script],
capture_output=True,
text=True,
timeout=60,
check=False,
)
assert result.returncode == 0, result.stdout + result.stderr
+92
View File
@@ -0,0 +1,92 @@
"""Contracts for explicit and automatic web-UI model loading settings."""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
from obliteratus.model_load_settings import resolve_model_load_settings
def _resolve(quantization="Auto (default)", dtype="Auto (default)", auto="4bit"):
calls = []
def automatic(resolved_dtype):
calls.append(resolved_dtype)
return auto
return resolve_model_load_settings(
quantization, dtype, auto_quantization=automatic,
), calls
def test_defaults_preserve_float16_and_automatic_quantization():
settings, calls = _resolve()
assert settings.dtype == "float16"
assert settings.quantization == "4bit"
assert calls == ["float16"]
def test_explicit_bfloat16_without_quantization_bypasses_auto_policy():
settings, calls = _resolve("None", "BF16")
assert settings.dtype == "bfloat16"
assert settings.quantization is None
assert calls == []
assert settings.summary == "quantization=none, compute dtype=bfloat16"
def test_explicit_four_bit_and_dtype_are_preserved():
settings, calls = _resolve("4-bit", "FP16", auto=None)
assert settings.quantization == "4bit"
assert settings.dtype == "float16"
assert calls == []
@pytest.mark.parametrize(
("quantization", "dtype", "match"),
[
("3-bit", "FP16", "Unsupported quantization choice"),
("None", "TF32", "Unsupported compute dtype"),
],
)
def test_invalid_choices_fail_early_with_actionable_errors(quantization, dtype, match):
with pytest.raises(ValueError, match=match):
_resolve(quantization, dtype)
def test_every_auto_quantization_ui_entry_point_uses_resolved_settings():
"""Guard every path that historically called app._should_quantize()."""
tree = ast.parse(Path("app.py").read_text())
functions = {
node.name: node
for node in ast.walk(tree)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}
for name in ("benchmark", "benchmark_multi_model", "obliterate", "strength_sweep"):
calls = {
node.func.id
for node in ast.walk(functions[name])
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
}
assert "_resolve_ui_load_settings" in calls, name
reload_calls = {
node.func.id
for node in ast.walk(functions["load_bench_into_chat"])
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
}
assert "_settings_from_metadata" in reload_calls
direct_auto_callers = []
for name, function in functions.items():
if any(
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "_should_quantize"
for node in ast.walk(function)
):
direct_auto_callers.append(name)
assert direct_auto_callers == ["_resolve_ui_load_settings"]
+4
View File
@@ -908,6 +908,8 @@ class TestTelemetryHubBoundaries:
{"refusal_rate": 0.0, "perplexity": None, "error": "partial"},
dataset="fixture",
n_prompts=4,
quantization="4bit",
compute_dtype="bfloat16",
pipeline_config={"n_directions": 3, "bayesian_trials": 2},
)
record = write.call_args.args[0]
@@ -916,6 +918,8 @@ class TestTelemetryHubBoundaries:
assert record.error == "partial"
assert record.n_directions == 3
assert record.use_bayesian is True
assert record.quantization == "4bit"
assert record.compute_dtype == "bfloat16"
def test_push_to_hub_success_and_empty_short_circuits(self, tmp_path):
import obliteratus.telemetry as telemetry