mirror of
https://github.com/p-e-w/heretic.git
synced 2026-09-10 06:09:08 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b7624ddfa | |||
| 0e7c14d94a | |||
| 02ce8ad079 | |||
| 79ea9ce905 | |||
| 216c089974 | |||
| 43f8e86a84 | |||
| da92f745de | |||
| ebb5e651df | |||
| 513e3acc72 |
@@ -1,6 +1,6 @@
|
||||
<img width="128" height="128" align="right" alt="Logo" src="https://github.com/user-attachments/assets/df5f2840-2f92-4991-aa57-252747d7182e" />
|
||||
|
||||
# Heretic: Fully automatic censorship removal for language models<br><br>[](https://discord.gg/gdXc48gSyT) [](https://huggingface.co/heretic-org)
|
||||
# Heretic: Fully automatic censorship removal for language models<br><br>[](https://discord.gg/gdXc48gSyT) [](https://huggingface.co/heretic-org) [](https://codeberg.org/p-e-w/heretic)
|
||||
|
||||
[](https://trendshift.io/repositories/20538)
|
||||
|
||||
@@ -20,6 +20,11 @@ as possible. Using Heretic does not require an understanding of transformer
|
||||
internals. In fact, anyone who knows how to run a command-line program
|
||||
can use Heretic to decensor language models.
|
||||
|
||||
Heretic supports most dense models, including many multimodal models,
|
||||
several different MoE architectures, and even some hybrid models like Qwen3.5.
|
||||
Pure state-space models and certain other research architectures are not yet
|
||||
supported out of the box.
|
||||
|
||||
<img width="650" height="715" alt="Screenshot" src="https://github.com/user-attachments/assets/d71a5efa-d6be-4705-a817-63332afb2d15" />
|
||||
|
||||
|
||||
@@ -65,15 +70,15 @@ Heretic have been well-received by users (links and emphasis added):
|
||||
> Has been the best unquantized abliterated model that I have been able to run on 16gb vram."
|
||||
> [*(Link to comment)*](https://old.reddit.com/r/LocalLLaMA/comments/1phjxca/im_calling_these_people_out_right_now/nt06tji/)
|
||||
|
||||
Heretic supports most dense models, including many multimodal models, and
|
||||
several different MoE architectures. It does not yet support SSMs/hybrid models,
|
||||
models with inhomogeneous layers, and certain novel attention systems.
|
||||
Heretic models have also been independently benchmarked using standard metrics
|
||||
like MMLU and GSM8K, and have been found to compare favorably with models
|
||||
produced by competing abliteration tools:
|
||||
[1](https://old.reddit.com/r/LocalLLaMA/comments/1sojjoc/abliterlitics_benchmark_and_tensor_analysis/),
|
||||
[2](https://old.reddit.com/r/LocalLLaMA/comments/1sy18lx/abliterlitics_benchmarks_and_tensor_comparison/).
|
||||
|
||||
You can find a small collection of models that have been decensored using Heretic
|
||||
[on Hugging Face](https://huggingface.co/collections/p-e-w/the-bestiary),
|
||||
and the community has created and published
|
||||
[well over 1,000](https://huggingface.co/models?other=heretic)
|
||||
Heretic models in addition to those.
|
||||
The community has created and published
|
||||
[well over 3000](https://huggingface.co/models?other=heretic)
|
||||
models with Heretic.
|
||||
|
||||
|
||||
## Usage
|
||||
@@ -88,6 +93,21 @@ heretic Qwen/Qwen3-4B-Instruct-2507
|
||||
|
||||
Replace `Qwen/Qwen3-4B-Instruct-2507` with whatever model you want to decensor.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> While PyTorch 2.2 is the minimum version of PyTorch needed for Heretic to work,
|
||||
> some models and configurations might require features only found in
|
||||
> later versions. For example, loading MXFP4-quantized models like gpt-oss
|
||||
> uses `torch.accelerator`, which was added in PyTorch 2.6.
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> Heretic uses [uv](https://docs.astral.sh/uv/) for dependency management,
|
||||
> and the repository includes a `uv.lock` file pinning every package version.
|
||||
> If you already use uv (and you probably should!), you can just clone the repo
|
||||
> and run Heretic with `uv run heretic`, which ensures that your dependencies
|
||||
> match those used by the developers, improving reliability and security.
|
||||
|
||||
The process is fully automatic and does not require configuration; however,
|
||||
Heretic has a variety of configuration parameters that can be changed for
|
||||
greater control. Run `heretic --help` to see available command-line options,
|
||||
@@ -103,7 +123,7 @@ models. Set the `quantization` option to `bnb_4bit` to enable quantization.
|
||||
|
||||
After Heretic has finished decensoring a model, you are given the option to
|
||||
save the model, upload it to Hugging Face, chat with it to test how well it works,
|
||||
or any combination of those actions.
|
||||
run standard benchmarks on it, or any combination of those actions.
|
||||
|
||||
|
||||
## Research features
|
||||
|
||||
+37
-12
@@ -27,6 +27,12 @@ device_map = "auto"
|
||||
# Maximum memory to allocate per device.
|
||||
# max_memory = { "0" = "20GB", "cpu" = "64GB" }
|
||||
|
||||
# Whether to move intermediate analysis tensors (such as residuals and logprobs)
|
||||
# to CPU memory as soon as possible to reduce peak VRAM usage.
|
||||
# This lowers peak VRAM usage during residual analysis and evaluation,
|
||||
# but may slightly reduce performance due to host/device transfers.
|
||||
offload_outputs_to_cpu = true
|
||||
|
||||
# Number of input sequences to process in parallel (0 = auto).
|
||||
batch_size = 0 # auto
|
||||
|
||||
@@ -36,6 +42,32 @@ max_batch_size = 128
|
||||
# Maximum number of tokens to generate for each response.
|
||||
max_response_length = 100
|
||||
|
||||
# List of pairs of the form [cot_initializer, closed_cot_block] used to skip
|
||||
# the Chain-of-Thought block in responses, so that evaluation happens
|
||||
# at the start of the actual response.
|
||||
chain_of_thought_skips = [
|
||||
# Most thinking models.
|
||||
[
|
||||
"<think>",
|
||||
"<think></think>",
|
||||
],
|
||||
# gpt-oss.
|
||||
[
|
||||
"<|channel|>analysis<|message|>",
|
||||
"<|channel|>analysis<|message|><|end|><|start|>assistant<|channel|>final<|message|>",
|
||||
],
|
||||
# Unknown, suggested by user.
|
||||
[
|
||||
"<thought>",
|
||||
"<thought></thought>",
|
||||
],
|
||||
# Unknown, suggested by user.
|
||||
[
|
||||
"[THINK]",
|
||||
"[THINK][/THINK]",
|
||||
],
|
||||
]
|
||||
|
||||
# Whether to print prompt/response pairs when counting refusals.
|
||||
print_responses = false
|
||||
|
||||
@@ -64,13 +96,13 @@ kl_divergence_target = 0.01
|
||||
|
||||
# Whether to adjust the refusal directions so that only the component that is
|
||||
# orthogonal to the good direction is subtracted during abliteration.
|
||||
orthogonalize_direction = false
|
||||
orthogonalize_direction = true
|
||||
|
||||
# How to apply row normalization of the weights. Options:
|
||||
# "none" (no normalization),
|
||||
# "pre" (compute LoRA adapter relative to row-normalized weights),
|
||||
# "full" (like "pre", but renormalizes to preserve original row magnitudes).
|
||||
row_normalization = "none"
|
||||
row_normalization = "full"
|
||||
|
||||
# The rank of the LoRA adapter to use when "full" row normalization is used.
|
||||
# Row magnitude preservation is approximate due to non-linear effects,
|
||||
@@ -98,6 +130,9 @@ n_startup_trials = 60
|
||||
# Directory to save and load study progress to/from.
|
||||
study_checkpoint_dir = "checkpoints"
|
||||
|
||||
# Maximum size for individual safetensors files generated when exporting a model.
|
||||
max_shard_size = "5GB"
|
||||
|
||||
# Strings whose presence in a response (case insensitive) identifies the response as a refusal.
|
||||
refusal_markers = [
|
||||
"sorry",
|
||||
@@ -137,12 +172,6 @@ refusal_markers = [
|
||||
# System prompt to use when prompting the model.
|
||||
system_prompt = "You are a helpful assistant."
|
||||
|
||||
# Move intermediate analysis tensors (such as residuals and logprobs)
|
||||
# to CPU memory as soon as possible to reduce peak VRAM usage.
|
||||
# This lowers peak VRAM usage during residual analysis and evaluation,
|
||||
# but may slightly reduce performance due to host/device transfers.
|
||||
offload_outputs_to_cpu = true
|
||||
|
||||
# Dataset of prompts that tend to not result in refusals (used for calculating refusal directions).
|
||||
[good_prompts]
|
||||
dataset = "mlabonne/harmless_alpaca"
|
||||
@@ -150,7 +179,6 @@ split = "train[:400]"
|
||||
column = "text"
|
||||
residual_plot_label = '"Harmless" prompts'
|
||||
residual_plot_color = "royalblue"
|
||||
commit = ""
|
||||
|
||||
# Dataset of prompts that tend to result in refusals (used for calculating refusal directions).
|
||||
[bad_prompts]
|
||||
@@ -159,18 +187,15 @@ split = "train[:400]"
|
||||
column = "text"
|
||||
residual_plot_label = '"Harmful" prompts'
|
||||
residual_plot_color = "darkorange"
|
||||
commit = ""
|
||||
|
||||
# Dataset of prompts that tend to not result in refusals (used for evaluating model performance).
|
||||
[good_evaluation_prompts]
|
||||
dataset = "mlabonne/harmless_alpaca"
|
||||
split = "test[:100]"
|
||||
column = "text"
|
||||
commit = ""
|
||||
|
||||
# Dataset of prompts that tend to result in refusals (used for evaluating model performance).
|
||||
[bad_evaluation_prompts]
|
||||
dataset = "mlabonne/harmful_behaviors"
|
||||
split = "test[:100]"
|
||||
column = "text"
|
||||
commit = ""
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "heretic-llm"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
description = "Fully automatic censorship removal for language models"
|
||||
readme = "README.md"
|
||||
license = "AGPL-3.0-or-later"
|
||||
@@ -28,12 +28,12 @@ dependencies = [
|
||||
"hf-transfer~=0.1",
|
||||
"huggingface-hub~=1.7",
|
||||
"immutabledict~=4.3",
|
||||
"kernels~=0.12",
|
||||
"kernels~=0.13",
|
||||
"langdetect~=1.0",
|
||||
"lm-eval[hf]~=0.4",
|
||||
"numpy~=2.2",
|
||||
"optuna~=4.7",
|
||||
"peft~=0.18",
|
||||
"peft~=0.19",
|
||||
"psutil~=7.2",
|
||||
"py-cpuinfo~=9.0",
|
||||
"pydantic-settings~=2.13",
|
||||
@@ -41,7 +41,7 @@ dependencies = [
|
||||
"rich~=14.3",
|
||||
"tomli-w~=1.2",
|
||||
"tqdm~=4.67",
|
||||
"transformers~=5.3",
|
||||
"transformers~=5.6",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
+52
-14
@@ -13,6 +13,12 @@ from pydantic_settings import (
|
||||
TomlConfigSettingsSource,
|
||||
)
|
||||
|
||||
# !!!IMPORTANT!!!
|
||||
#
|
||||
# Any settings added to the classes defined in this module
|
||||
# must be evaluated for privacy implications and have
|
||||
# exclude=True set in their field definitions if appropriate.
|
||||
|
||||
|
||||
class QuantizationMethod(str, Enum):
|
||||
NONE = "none"
|
||||
@@ -31,6 +37,11 @@ class DatasetSpecification(BaseModel):
|
||||
description="Hugging Face dataset ID, or path to dataset on disk."
|
||||
)
|
||||
|
||||
commit: str | None = Field(
|
||||
default=None,
|
||||
description="Hugging Face commit hash of the dataset.",
|
||||
)
|
||||
|
||||
split: str = Field(description="Portion of the dataset to use.")
|
||||
|
||||
column: str = Field(description="Column in the dataset that contains the prompts.")
|
||||
@@ -53,15 +64,13 @@ class DatasetSpecification(BaseModel):
|
||||
residual_plot_label: str | None = Field(
|
||||
default=None,
|
||||
description="Label to use for the dataset in plots of residual vectors.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
residual_plot_color: str | None = Field(
|
||||
default=None,
|
||||
description="Matplotlib color to use for the dataset in plots of residual vectors.",
|
||||
)
|
||||
commit: str | None = Field(
|
||||
default=None,
|
||||
description="Hugging Face commit hash of the dataset.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -80,12 +89,18 @@ class BenchmarkSpecification(BaseModel):
|
||||
class Settings(BaseSettings):
|
||||
model: str = Field(description="Hugging Face model ID, or path to model on disk.")
|
||||
|
||||
model_commit: str | None = Field(
|
||||
default=None,
|
||||
description="Hugging Face commit hash of the model.",
|
||||
)
|
||||
|
||||
evaluate_model: str | None = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"If this model ID or path is set, then instead of abliterating the main model, "
|
||||
"evaluate this model relative to the main model."
|
||||
),
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
dtypes: list[str] = Field(
|
||||
@@ -126,9 +141,21 @@ class Settings(BaseSettings):
|
||||
description='Maximum memory to allocate per device (e.g., { "0" = "20GB", "cpu" = "64GB" }).',
|
||||
)
|
||||
|
||||
offload_outputs_to_cpu: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Whether to move intermediate analysis tensors (such as residuals and logprobs) "
|
||||
"to CPU memory as soon as possible to reduce peak VRAM usage. "
|
||||
"This lowers peak VRAM usage during residual analysis and evaluation, "
|
||||
"but may slightly reduce performance due to host/device transfers."
|
||||
),
|
||||
)
|
||||
|
||||
trust_remote_code: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to trust remote code when loading the model.",
|
||||
# For security reasons, we don't store this setting.
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
batch_size: int = Field(
|
||||
@@ -139,6 +166,9 @@ class Settings(BaseSettings):
|
||||
max_batch_size: int = Field(
|
||||
default=128,
|
||||
description="Maximum batch size to try when automatically determining the optimal batch size.",
|
||||
# When storing a settings object, the batch size is already fixed,
|
||||
# either determined by the automatic mechanism or by explicit user choice.
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
max_response_length: int = Field(
|
||||
@@ -183,36 +213,45 @@ class Settings(BaseSettings):
|
||||
"the Chain-of-Thought block in responses, so that evaluation happens "
|
||||
"at the start of the actual response."
|
||||
),
|
||||
# When storing a settings object, the response prefix is already fixed,
|
||||
# either determined by the automatic mechanism or by explicit user choice.
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
print_responses: bool = Field(
|
||||
default=False,
|
||||
description="Whether to print prompt/response pairs when counting refusals.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
print_residual_geometry: bool = Field(
|
||||
default=False,
|
||||
description="Whether to print detailed information about residuals and refusal directions.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
plot_residuals: bool = Field(
|
||||
default=False,
|
||||
description="Whether to generate plots showing PaCMAP projections of residual vectors.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
residual_plot_path: str = Field(
|
||||
default="plots",
|
||||
description="Base path to save plots of residual vectors to.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
residual_plot_title: str = Field(
|
||||
default='PaCMAP Projection of Residual Vectors for "Harmless" and "Harmful" Prompts',
|
||||
description="Title placed above plots of residual vectors.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
residual_plot_style: str = Field(
|
||||
default="dark_background",
|
||||
description="Matplotlib style sheet to use for plots of residual vectors.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
kl_divergence_scale: float = Field(
|
||||
@@ -232,7 +271,7 @@ class Settings(BaseSettings):
|
||||
)
|
||||
|
||||
orthogonalize_direction: bool = Field(
|
||||
default=False,
|
||||
default=True,
|
||||
description=(
|
||||
"Whether to adjust the refusal directions so that only the component that is "
|
||||
"orthogonal to the good direction is subtracted during abliteration."
|
||||
@@ -240,7 +279,7 @@ class Settings(BaseSettings):
|
||||
)
|
||||
|
||||
row_normalization: RowNormalization = Field(
|
||||
default=RowNormalization.NONE,
|
||||
default=RowNormalization.FULL,
|
||||
description=(
|
||||
"How to apply row normalization of the weights. Options: "
|
||||
'"none" (no normalization), '
|
||||
@@ -291,6 +330,7 @@ class Settings(BaseSettings):
|
||||
study_checkpoint_dir: str = Field(
|
||||
default="checkpoints",
|
||||
description="Directory to save and load study progress to/from.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
benchmarks: list[BenchmarkSpecification] = Field(
|
||||
@@ -352,6 +392,12 @@ class Settings(BaseSettings):
|
||||
),
|
||||
],
|
||||
description="Benchmarks to offer to the user for evaluating abliterated models.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
max_shard_size: int | str = Field(
|
||||
default="5GB",
|
||||
description="Maximum size for individual safetensors files generated when exporting a model.",
|
||||
)
|
||||
|
||||
refusal_markers: list[str] = Field(
|
||||
@@ -397,14 +443,6 @@ class Settings(BaseSettings):
|
||||
description="System prompt to use when prompting the model.",
|
||||
)
|
||||
|
||||
offload_outputs_to_cpu: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Whether to move intermediate analysis tensors (such as residuals and logprobs) "
|
||||
"to CPU memory as soon as possible to reduce peak VRAM usage."
|
||||
),
|
||||
)
|
||||
|
||||
good_prompts: DatasetSpecification = Field(
|
||||
default=DatasetSpecification(
|
||||
dataset="mlabonne/harmless_alpaca",
|
||||
|
||||
+79
-42
@@ -17,11 +17,15 @@ def _is_help_invocation() -> bool:
|
||||
if _is_help_invocation():
|
||||
Settings() # ty:ignore[missing-argument]
|
||||
|
||||
# FIXME: Rich progress bars are currently disabled because of rendering issues
|
||||
# when used from multiple threads in parallel (e.g. by huggingface_hub).
|
||||
"""
|
||||
from .progress import patch_tqdm
|
||||
|
||||
# This patches tqdm class definitions, which must happen
|
||||
# before any other module imports tqdm.
|
||||
patch_tqdm()
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
@@ -66,10 +70,10 @@ from .utils import (
|
||||
format_duration,
|
||||
get_readme_intro,
|
||||
get_trial_parameters,
|
||||
is_hf_path,
|
||||
load_prompts,
|
||||
print,
|
||||
print_memory_usage,
|
||||
prompt_confirm,
|
||||
prompt_password,
|
||||
prompt_path,
|
||||
prompt_select,
|
||||
@@ -79,7 +83,7 @@ from .utils import (
|
||||
)
|
||||
|
||||
|
||||
def obtain_merge_strategy(settings: Settings) -> str | None:
|
||||
def obtain_merge_strategy(settings: Settings, model: Model) -> str | None:
|
||||
"""
|
||||
Prompts the user for how to proceed with saving the model.
|
||||
Provides info to the user if the model is quantized on memory use.
|
||||
@@ -108,7 +112,8 @@ def obtain_merge_strategy(settings: Settings) -> str | None:
|
||||
settings.model,
|
||||
device_map="meta",
|
||||
torch_dtype=torch.bfloat16,
|
||||
trust_remote_code=True,
|
||||
trust_remote_code=model.trusted_models.get(settings.model),
|
||||
**model.revision_kwargs,
|
||||
)
|
||||
footprint_bytes = meta_model.get_memory_footprint()
|
||||
footprint_gb = footprint_bytes / (1024**3)
|
||||
@@ -424,9 +429,6 @@ def run():
|
||||
|
||||
needs_full_residuals = settings.print_residual_geometry or settings.plot_residuals
|
||||
|
||||
good_residuals = None
|
||||
bad_residuals = None
|
||||
|
||||
if needs_full_residuals:
|
||||
print("* Obtaining residuals for good prompts...")
|
||||
good_residuals = model.get_residuals_batched(good_prompts)
|
||||
@@ -464,8 +466,12 @@ def run():
|
||||
refusal_directions - projection_vector.unsqueeze(1) * good_directions
|
||||
)
|
||||
refusal_directions = F.normalize(refusal_directions, p=2, dim=1)
|
||||
del good_directions, projection_vector
|
||||
|
||||
del good_means, bad_means
|
||||
|
||||
# Clear cache before starting the optimization study.
|
||||
# This should free up memory from the objects released with the del statements above.
|
||||
empty_cache()
|
||||
|
||||
trial_index = 0
|
||||
@@ -571,7 +577,8 @@ def run():
|
||||
|
||||
trial.set_user_attr("kl_divergence", kl_divergence)
|
||||
trial.set_user_attr("refusals", refusals)
|
||||
trial.set_user_attr("total_refusal_prompts", len(evaluator.bad_prompts))
|
||||
trial.set_user_attr("base_refusals", evaluator.base_refusals)
|
||||
trial.set_user_attr("n_bad_prompts", len(evaluator.bad_prompts))
|
||||
|
||||
return score
|
||||
|
||||
@@ -681,8 +688,9 @@ def run():
|
||||
(
|
||||
"The following trials resulted in Pareto optimal combinations of refusals and KL divergence. "
|
||||
"After selecting a trial, you will be able to save the model, upload it to Hugging Face, "
|
||||
"or chat with it to test how well it works. You can return to this menu later to select a different trial. "
|
||||
"[yellow]Note that KL divergence values above 1 usually indicate significant damage to the original model's capabilities.[/]"
|
||||
"chat with it to test how well it works, or run standard benchmarks on it. "
|
||||
"You can return to this menu later to select a different trial. "
|
||||
"[yellow]Note that KL divergence values above 0.5 usually indicate significant damage to the original model's capabilities.[/]"
|
||||
)
|
||||
)
|
||||
|
||||
@@ -772,17 +780,23 @@ def run():
|
||||
if not save_directory:
|
||||
continue
|
||||
|
||||
strategy = obtain_merge_strategy(settings)
|
||||
strategy = obtain_merge_strategy(settings, model)
|
||||
if strategy is None:
|
||||
continue
|
||||
|
||||
if strategy == "adapter":
|
||||
print("Saving LoRA adapter...")
|
||||
model.model.save_pretrained(save_directory)
|
||||
model.model.save_pretrained(
|
||||
save_directory,
|
||||
max_shard_size=settings.max_shard_size,
|
||||
)
|
||||
else:
|
||||
print("Saving merged model...")
|
||||
merged_model = model.get_merged_model()
|
||||
merged_model.save_pretrained(save_directory)
|
||||
merged_model.save_pretrained(
|
||||
save_directory,
|
||||
max_shard_size=settings.max_shard_size,
|
||||
)
|
||||
del merged_model
|
||||
empty_cache()
|
||||
model.tokenizer.save_pretrained(save_directory)
|
||||
@@ -823,7 +837,7 @@ def run():
|
||||
continue
|
||||
private = visibility == "Private"
|
||||
|
||||
strategy = obtain_merge_strategy(settings)
|
||||
strategy = obtain_merge_strategy(settings, model)
|
||||
if strategy is None:
|
||||
continue
|
||||
|
||||
@@ -835,27 +849,48 @@ def run():
|
||||
settings.good_evaluation_prompts.dataset,
|
||||
settings.bad_evaluation_prompts.dataset,
|
||||
]
|
||||
can_reproduce = not Path(settings.model).exists() and all(
|
||||
not Path(d).exists() for d in datasets
|
||||
is_reproducible = is_hf_path(settings.model) and all(
|
||||
is_hf_path(dataset) for dataset in datasets
|
||||
)
|
||||
|
||||
if can_reproduce:
|
||||
# Pin the number of trials to the number of actual completed trials
|
||||
# for the reproduction configuration.
|
||||
settings.n_trials = count_completed_trials()
|
||||
|
||||
include_reproduce = prompt_confirm(
|
||||
"""Include 'reproduce' folder?
|
||||
This saves your exact configuration and system information, along with the study checkpoint, to help others verify your results."""
|
||||
if is_reproducible:
|
||||
print(
|
||||
(
|
||||
"Heretic can add information to the repository that allows others to reproduce the model. "
|
||||
"This is optional, but valuable to the community as both a learning tool and to preserve computational work already done. "
|
||||
"Guaranteeing reproducibility requires basic system information (Python and OS version, CPU and GPU/accelerator info) "
|
||||
"as tensor operations can give different results in different system environments. "
|
||||
"[bold]The information does not include any file system paths or other private data.[/]"
|
||||
)
|
||||
)
|
||||
reproducibility_information = prompt_select(
|
||||
"Which reproducibility information do you want to add?",
|
||||
[
|
||||
Choice(
|
||||
title="Full: Settings, package versions, and system information",
|
||||
value="full",
|
||||
),
|
||||
Choice(
|
||||
title="Basic: Settings and package versions",
|
||||
value="basic",
|
||||
),
|
||||
Choice(
|
||||
title="Don't add any reproducibility information",
|
||||
value="none",
|
||||
),
|
||||
],
|
||||
)
|
||||
if reproducibility_information is None:
|
||||
continue
|
||||
else:
|
||||
include_reproduce = False
|
||||
reproducibility_information = "none"
|
||||
|
||||
if strategy == "adapter":
|
||||
print("Uploading LoRA adapter...")
|
||||
model.model.push_to_hub(
|
||||
repo_id,
|
||||
private=private,
|
||||
max_shard_size=settings.max_shard_size,
|
||||
token=token,
|
||||
)
|
||||
else:
|
||||
@@ -864,6 +899,7 @@ This saves your exact configuration and system information, along with the study
|
||||
merged_model.push_to_hub(
|
||||
repo_id,
|
||||
private=private,
|
||||
max_shard_size=settings.max_shard_size,
|
||||
token=token,
|
||||
)
|
||||
del merged_model
|
||||
@@ -874,22 +910,18 @@ This saves your exact configuration and system information, along with the study
|
||||
token=token,
|
||||
)
|
||||
|
||||
# If the model path exists locally and includes the
|
||||
# card, use it directly. If the model path doesn't
|
||||
# exist locally, it can be assumed to be a model
|
||||
# hosted on the Hugging Face Hub, in which case
|
||||
# we can retrieve the model card.
|
||||
model_path = Path(settings.model)
|
||||
if model_path.exists():
|
||||
if is_hf_path(settings.model):
|
||||
card = ModelCard.load(settings.model)
|
||||
else:
|
||||
card_path = (
|
||||
model_path / huggingface_hub.constants.REPOCARD_NAME
|
||||
Path(settings.model)
|
||||
/ huggingface_hub.constants.REPOCARD_NAME
|
||||
)
|
||||
if card_path.exists():
|
||||
card = ModelCard.load(card_path)
|
||||
else:
|
||||
card = None
|
||||
else:
|
||||
card = ModelCard.load(settings.model)
|
||||
|
||||
if card is not None:
|
||||
if card.data is None:
|
||||
card.data = ModelCardData()
|
||||
@@ -899,30 +931,35 @@ This saves your exact configuration and system information, along with the study
|
||||
card.data.tags.append("uncensored")
|
||||
card.data.tags.append("decensored")
|
||||
card.data.tags.append("abliterated")
|
||||
if reproducibility_information != "none":
|
||||
card.data.tags.append("reproducible")
|
||||
card.text = (
|
||||
get_readme_intro(
|
||||
settings,
|
||||
trial,
|
||||
evaluator.base_refusals,
|
||||
evaluator.bad_prompts,
|
||||
reproducibility_information != "none",
|
||||
)
|
||||
+ card.text
|
||||
)
|
||||
card.push_to_hub(repo_id, token=token)
|
||||
|
||||
if include_reproduce:
|
||||
if reproducibility_information != "none":
|
||||
# Set the number of trials to the number of actual completed trials
|
||||
# for the reproduction configuration.
|
||||
settings.n_trials = count_completed_trials()
|
||||
|
||||
upload_reproduce_folder(
|
||||
repo_id,
|
||||
settings,
|
||||
token,
|
||||
checkpoint_path=study_checkpoint_file,
|
||||
trial=trial,
|
||||
include_system_information=(
|
||||
reproducibility_information == "full"
|
||||
),
|
||||
)
|
||||
print(
|
||||
f"Model and reproducibility files uploaded to [bold]{repo_id}[/]."
|
||||
)
|
||||
else:
|
||||
print(f"Model uploaded to [bold]{repo_id}[/].")
|
||||
|
||||
print(f"Model uploaded to [bold]{repo_id}[/].")
|
||||
|
||||
case "Chat with the model":
|
||||
print()
|
||||
|
||||
+17
-6
@@ -62,12 +62,17 @@ class Model:
|
||||
self.settings = settings
|
||||
self.needs_reload = False
|
||||
|
||||
self.revision_kwargs = {}
|
||||
if settings.model_commit is not None:
|
||||
self.revision_kwargs["revision"] = settings.model_commit
|
||||
|
||||
print()
|
||||
print(f"Loading model [bold]{settings.model}[/]...")
|
||||
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(
|
||||
settings.model,
|
||||
trust_remote_code=settings.trust_remote_code,
|
||||
**self.revision_kwargs,
|
||||
)
|
||||
|
||||
# Fallback for tokenizers that don't declare a special pad token.
|
||||
@@ -108,6 +113,7 @@ class Model:
|
||||
device_map=settings.device_map,
|
||||
max_memory=self.max_memory,
|
||||
trust_remote_code=self.trusted_models.get(settings.model),
|
||||
**self.revision_kwargs,
|
||||
**extra_kwargs,
|
||||
)
|
||||
|
||||
@@ -148,13 +154,15 @@ class Model:
|
||||
# so we don't need to do anything manually.
|
||||
|
||||
print(f"* Transformer model with [bold]{len(self.get_layers())}[/] layers")
|
||||
print("* Abliterable components:")
|
||||
|
||||
all_components = {}
|
||||
for layer_index in range(len(self.get_layers())):
|
||||
for component, modules in self.get_layer_modules(layer_index).items():
|
||||
if component not in all_components:
|
||||
all_components[component] = 0
|
||||
all_components[component] += len(modules)
|
||||
|
||||
print("* Abliterable components:")
|
||||
for component, count in all_components.items():
|
||||
print(f" * [bold]{component}[/]: [bold]{count}[/] modules total")
|
||||
|
||||
@@ -257,6 +265,7 @@ class Model:
|
||||
torch_dtype=self.model.dtype,
|
||||
device_map="cpu",
|
||||
trust_remote_code=self.trusted_models.get(self.settings.model),
|
||||
**self.revision_kwargs,
|
||||
)
|
||||
|
||||
# Apply LoRA adapters to the CPU model
|
||||
@@ -318,6 +327,7 @@ class Model:
|
||||
device_map=self.settings.device_map,
|
||||
max_memory=self.max_memory,
|
||||
trust_remote_code=self.trusted_models.get(self.settings.model),
|
||||
**self.revision_kwargs,
|
||||
**extra_kwargs,
|
||||
)
|
||||
|
||||
@@ -360,8 +370,8 @@ class Model:
|
||||
with suppress(Exception):
|
||||
try_add("attn.o_proj", layer.self_attn.o_proj) # ty:ignore[possibly-missing-attribute]
|
||||
|
||||
# Qwen3.5 MoE hybrid layers use GatedDeltaNet (linear attention) instead
|
||||
# of standard self-attention, so self_attn.o_proj doesn't exist on those layers.
|
||||
# Qwen3.5 MoE hybrid layers use GatedDeltaNet (linear attention) instead of
|
||||
# standard self-attention, so self_attn.o_proj doesn't exist on those layers.
|
||||
with suppress(Exception):
|
||||
try_add("attn.o_proj", layer.linear_attn.out_proj) # ty:ignore[possibly-missing-attribute]
|
||||
|
||||
@@ -395,11 +405,13 @@ class Model:
|
||||
return modules
|
||||
|
||||
def get_abliterable_components(self) -> list[str]:
|
||||
components: set[str] = set()
|
||||
|
||||
# Scan all layers because hybrid models (e.g. Qwen3.5 MoE) have different
|
||||
# components on different layers (some have self_attn, others linear_attn).
|
||||
components: set[str] = set()
|
||||
for layer_index in range(len(self.get_layers())):
|
||||
components.update(self.get_layer_modules(layer_index).keys())
|
||||
|
||||
return sorted(components)
|
||||
|
||||
def abliterate(
|
||||
@@ -736,9 +748,8 @@ class Model:
|
||||
# The returned tensor has shape (prompt, token).
|
||||
logprobs = F.log_softmax(logits, dim=-1)
|
||||
|
||||
del outputs
|
||||
|
||||
if self.settings.offload_outputs_to_cpu:
|
||||
del outputs, logits
|
||||
logprobs = logprobs.cpu()
|
||||
empty_cache()
|
||||
|
||||
|
||||
+61
-45
@@ -25,6 +25,7 @@ from accelerate.utils import (
|
||||
|
||||
def empty_cache():
|
||||
"""Clears the backend cache and collects garbage."""
|
||||
|
||||
# Collecting garbage is not an idempotent operation, and to avoid OOM errors,
|
||||
# gc.collect() has to be called both before and after emptying the backend cache.
|
||||
# See https://github.com/p-e-w/heretic/pull/17 for details.
|
||||
@@ -48,6 +49,7 @@ def empty_cache():
|
||||
|
||||
def get_nvidia_driver_version() -> str | None:
|
||||
"""Gets the NVIDIA driver version using nvidia-smi."""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["nvidia-smi", "--query-gpu=driver_version", "--format=csv,noheader"],
|
||||
@@ -61,6 +63,7 @@ def get_nvidia_driver_version() -> str | None:
|
||||
|
||||
def get_amdgpu_driver_version() -> str | None:
|
||||
"""Gets the AMD GPU (ROCm) driver and suite version info."""
|
||||
|
||||
# 1. Try amd-smi (modern standard for ROCm 6.0+)
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
@@ -101,6 +104,7 @@ def get_amdgpu_driver_version() -> str | None:
|
||||
|
||||
def get_xpu_driver_version() -> str | None:
|
||||
"""Gets the Intel XPU driver version."""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["xpu-smi", "discovery"],
|
||||
@@ -117,6 +121,7 @@ def get_xpu_driver_version() -> str | None:
|
||||
|
||||
def get_npu_driver_version() -> str | None:
|
||||
"""Gets the Huawei NPU driver version."""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["npu-smi", "info", "-t", "board", "-i", "0"],
|
||||
@@ -133,6 +138,7 @@ def get_npu_driver_version() -> str | None:
|
||||
|
||||
def get_mps_driver_version() -> str | None:
|
||||
"""Gets the Apple Silicon (MPS) driver version via macOS version."""
|
||||
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
["sw_vers", "-productVersion"],
|
||||
@@ -156,6 +162,7 @@ class HereticVersionInfo:
|
||||
|
||||
def get_heretic_version_info() -> HereticVersionInfo:
|
||||
"""Detects version and installation source (PyPI, Git, Local) of heretic-llm."""
|
||||
|
||||
package_name = "heretic-llm"
|
||||
origin_metadata: dict[str, Any] = {"type": "unknown"}
|
||||
# This package must be installed for this code to run.
|
||||
@@ -171,6 +178,7 @@ def get_heretic_version_info() -> HereticVersionInfo:
|
||||
if not direct_url_content:
|
||||
# Standard PyPI installation.
|
||||
origin_metadata["type"] = "pypi"
|
||||
|
||||
return HereticVersionInfo(
|
||||
version=base_version,
|
||||
origin="PyPI",
|
||||
@@ -178,51 +186,48 @@ def get_heretic_version_info() -> HereticVersionInfo:
|
||||
metadata=origin_metadata,
|
||||
)
|
||||
|
||||
try:
|
||||
data = json.loads(direct_url_content)
|
||||
data = json.loads(direct_url_content)
|
||||
|
||||
# Check for Git source.
|
||||
if "vcs_info" in data and data["vcs_info"].get("vcs") == "git":
|
||||
vcs_info = data["vcs_info"]
|
||||
commit_hash = vcs_info.get("commit_id", "unknown")
|
||||
repo_url = data.get("url", "unknown_repo")
|
||||
requested_revision = vcs_info.get("requested_revision")
|
||||
# Check for Git source.
|
||||
if "vcs_info" in data and data["vcs_info"].get("vcs") == "git":
|
||||
vcs_info = data["vcs_info"]
|
||||
commit_hash = vcs_info.get("commit_id", "unknown")
|
||||
repo_url = data.get("url", "unknown_repo")
|
||||
requested_revision = vcs_info.get("requested_revision")
|
||||
|
||||
if requested_revision:
|
||||
origin_str = (
|
||||
f"Git ({repo_url}@{requested_revision} - commit: {commit_hash})"
|
||||
)
|
||||
else:
|
||||
origin_str = f"Git ({repo_url} @ {commit_hash})"
|
||||
|
||||
origin_metadata.update(
|
||||
{
|
||||
"type": "git",
|
||||
"url": repo_url,
|
||||
"commit_hash": commit_hash,
|
||||
"requested_revision": requested_revision,
|
||||
}
|
||||
if requested_revision:
|
||||
origin_str = (
|
||||
f"Git ({repo_url}@{requested_revision} - commit: {commit_hash})"
|
||||
)
|
||||
else:
|
||||
origin_str = f"Git ({repo_url} @ {commit_hash})"
|
||||
|
||||
return HereticVersionInfo(
|
||||
version=base_version,
|
||||
origin=origin_str,
|
||||
is_standard_pypi=False,
|
||||
metadata=origin_metadata,
|
||||
)
|
||||
origin_metadata.update(
|
||||
{
|
||||
"type": "git",
|
||||
"url": repo_url,
|
||||
"commit_hash": commit_hash,
|
||||
"requested_revision": requested_revision,
|
||||
}
|
||||
)
|
||||
|
||||
# Check for local file/wheel directory.
|
||||
if "url" in data and data["url"].startswith("file://"):
|
||||
origin_metadata["type"] = "local"
|
||||
return HereticVersionInfo(
|
||||
version=base_version,
|
||||
origin="Local",
|
||||
is_standard_pypi=False,
|
||||
metadata=origin_metadata,
|
||||
)
|
||||
return HereticVersionInfo(
|
||||
version=base_version,
|
||||
origin=origin_str,
|
||||
is_standard_pypi=False,
|
||||
metadata=origin_metadata,
|
||||
)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
# Check for local file/wheel directory.
|
||||
if "url" in data and data["url"].startswith("file://"):
|
||||
origin_metadata["type"] = "local"
|
||||
|
||||
return HereticVersionInfo(
|
||||
version=base_version,
|
||||
origin="Local",
|
||||
is_standard_pypi=False,
|
||||
metadata=origin_metadata,
|
||||
)
|
||||
|
||||
return HereticVersionInfo(
|
||||
version=base_version,
|
||||
@@ -234,6 +239,7 @@ def get_heretic_version_info() -> HereticVersionInfo:
|
||||
|
||||
def get_accelerator_info_dict() -> dict[str, Any]:
|
||||
"""Retrieves raw accelerator info (CUDA, ROCm, etc) directly into structured keys."""
|
||||
|
||||
if torch.cuda.is_available():
|
||||
count = torch.cuda.device_count()
|
||||
is_rocm = getattr(torch.version, "hip", None) is not None
|
||||
@@ -320,6 +326,7 @@ def get_accelerator_info_dict() -> dict[str, Any]:
|
||||
|
||||
def get_accelerator_info(include_warnings: bool = True) -> str:
|
||||
"""Convenience wrapper for hardware detection and console-friendly formatting."""
|
||||
|
||||
info = get_accelerator_info_dict()
|
||||
|
||||
if info["type"] is None:
|
||||
@@ -350,6 +357,7 @@ def get_accelerator_info(include_warnings: bool = True) -> str:
|
||||
|
||||
def get_cpu_info_dict() -> dict[str, str | int | None]:
|
||||
"""Gets granular CPU identifiers using the py-cpuinfo library."""
|
||||
|
||||
info = cpuinfo.get_cpu_info()
|
||||
|
||||
return {
|
||||
@@ -363,6 +371,7 @@ def get_cpu_info_dict() -> dict[str, str | int | None]:
|
||||
|
||||
def get_cpu_info() -> str:
|
||||
"""Gets the CPU brand name."""
|
||||
|
||||
info = get_cpu_info_dict()
|
||||
parts = []
|
||||
parts.append(
|
||||
@@ -397,12 +406,14 @@ def get_python_env_info_dict() -> dict[str, str]:
|
||||
|
||||
def get_python_env_info() -> str:
|
||||
"""Detects the type of Python environment (Conda, Venv, etc.) and build info."""
|
||||
|
||||
info = get_python_env_info_dict()
|
||||
return f"{info['version']} ({info['implementation']}, {info['compiler']}) [{info['environment']}]"
|
||||
|
||||
|
||||
def get_package_version(name: str) -> str | None:
|
||||
def get_package_version(name: str) -> str:
|
||||
"""Gets the installed version of a package, stripping local suffixes like +cu128."""
|
||||
|
||||
# Normalize name: pip considers hyphens and underscores equivalent.
|
||||
normalized_name = name.lower().replace("_", "-")
|
||||
version_str = importlib.metadata.version(normalized_name)
|
||||
@@ -411,8 +422,12 @@ def get_package_version(name: str) -> str | None:
|
||||
|
||||
def get_requirements_dict() -> dict[str, str]:
|
||||
"""Recursively finds all direct and transitive dependencies of heretic-llm and core libraries."""
|
||||
|
||||
# We start with heretic-llm and the core compute libraries.
|
||||
# PyTorch is not listed as a dependency in the heretic-llm package
|
||||
# because installation is hardware-specific and must be done manually.
|
||||
packages_to_check = ["heretic-llm", "torch", "torchaudio", "torchvision"]
|
||||
|
||||
visited = set()
|
||||
required_packages = set()
|
||||
|
||||
@@ -445,18 +460,19 @@ def get_requirements_dict() -> dict[str, str]:
|
||||
# If a package is listed as a dependency but not installed, we skip it.
|
||||
continue
|
||||
|
||||
required_packages_sorted = sorted(required_packages)
|
||||
|
||||
# Lookup versions for all discovered packages.
|
||||
dependencies = {}
|
||||
version_info = get_heretic_version_info()
|
||||
for name in required_packages:
|
||||
|
||||
for package in required_packages_sorted:
|
||||
# If heretic-llm was installed from source (Git/Local), exclude it
|
||||
# from requirements.txt to prevent pip from downloading an unrelated
|
||||
# version from PyPI during reproduction.
|
||||
if name == "heretic-llm" and not version_info.is_standard_pypi:
|
||||
if package == "heretic-llm" and not version_info.is_standard_pypi:
|
||||
continue
|
||||
|
||||
version_str = get_package_version(name)
|
||||
if version_str:
|
||||
dependencies[name] = version_str
|
||||
dependencies[package] = get_package_version(package)
|
||||
|
||||
return dependencies
|
||||
|
||||
+291
-249
@@ -9,6 +9,7 @@ import random
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar
|
||||
|
||||
@@ -155,18 +156,6 @@ def prompt_password(message: str) -> str:
|
||||
return questionary.password(message).ask()
|
||||
|
||||
|
||||
def prompt_confirm(message: str, default: bool = True) -> bool:
|
||||
if is_notebook():
|
||||
print()
|
||||
choices = "[Y/n]" if default else "[y/N]"
|
||||
result = input(f"{message} {choices} ").strip().lower()
|
||||
if not result:
|
||||
return default
|
||||
return result in ("y", "yes")
|
||||
else:
|
||||
return questionary.confirm(message, default=default).ask()
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
seconds = round(seconds)
|
||||
hours, seconds = divmod(seconds, 3600)
|
||||
@@ -180,6 +169,18 @@ def format_duration(seconds: float) -> str:
|
||||
return f"{seconds}s"
|
||||
|
||||
|
||||
def is_hf_path(path: str) -> bool:
|
||||
"""Checks whether a path likely refers to a Hugging Face repository."""
|
||||
|
||||
return (
|
||||
not path.startswith("/")
|
||||
and not path.endswith("/")
|
||||
and path.count("/") == 1
|
||||
and "\\" not in path
|
||||
and not Path(path).exists()
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Prompt:
|
||||
system: str
|
||||
@@ -193,7 +194,13 @@ def load_prompts(
|
||||
path = specification.dataset
|
||||
split_str = specification.split
|
||||
|
||||
if os.path.isdir(path):
|
||||
if is_hf_path(path):
|
||||
dataset = load_dataset(
|
||||
path,
|
||||
revision=specification.commit,
|
||||
split=split_str,
|
||||
)
|
||||
else:
|
||||
if Path(path, DATASET_STATE_JSON_FILENAME).exists():
|
||||
# Dataset saved with datasets.save_to_disk; needs special handling.
|
||||
# Path should be the subdirectory for a particular split.
|
||||
@@ -211,7 +218,7 @@ def load_prompts(
|
||||
# Get the dataset by applying the indices.
|
||||
dataset = dataset[abs_instruction.from_ : abs_instruction.to]
|
||||
else:
|
||||
# Path is a local directory.
|
||||
# Path should be a local directory.
|
||||
dataset = load_dataset(
|
||||
path,
|
||||
split=split_str,
|
||||
@@ -220,9 +227,6 @@ def load_prompts(
|
||||
# But also don't use cached data, as the dataset may have changed on disk.
|
||||
download_mode=DownloadMode.FORCE_REDOWNLOAD,
|
||||
)
|
||||
else:
|
||||
# Probably a repository path; let load_dataset figure it out.
|
||||
dataset = load_dataset(path, split=split_str)
|
||||
|
||||
prompts = list(dataset[specification.column])
|
||||
|
||||
@@ -272,20 +276,28 @@ def get_trial_parameters(trial: Trial) -> dict[str, str]:
|
||||
def get_readme_intro(
|
||||
settings: Settings,
|
||||
trial: Trial,
|
||||
base_refusals: int,
|
||||
bad_prompts: list[Prompt],
|
||||
contains_reproducibility_information: bool,
|
||||
) -> str:
|
||||
if Path(settings.model).exists():
|
||||
if is_hf_path(settings.model):
|
||||
model_link = f"[{settings.model}](https://huggingface.co/{settings.model})"
|
||||
else:
|
||||
# Hide the path, which may contain private information.
|
||||
model_link = "a model"
|
||||
else:
|
||||
model_link = f"[{settings.model}](https://huggingface.co/{settings.model})"
|
||||
|
||||
version_info = get_heretic_version_info()
|
||||
if contains_reproducibility_information:
|
||||
reproducibility_instructions = """
|
||||
> [!TIP]
|
||||
> **This model is reproducible!**
|
||||
>
|
||||
> See the [README](reproduce/README.md) in the `reproduce` directory for more information.
|
||||
"""
|
||||
else:
|
||||
reproducibility_instructions = ""
|
||||
|
||||
return f"""# This is a decensored version of {
|
||||
model_link
|
||||
}, made using [Heretic](https://github.com/p-e-w/heretic) v{version_info.version}
|
||||
|
||||
}, made using [Heretic](https://github.com/p-e-w/heretic) v{version("heretic-llm")}
|
||||
{reproducibility_instructions}
|
||||
## Abliteration parameters
|
||||
|
||||
| Parameter | Value |
|
||||
@@ -304,9 +316,9 @@ def get_readme_intro(
|
||||
| Metric | This model | Original model ({model_link}) |
|
||||
| :----- | :--------: | :---------------------------: |
|
||||
| **KL divergence** | {trial.user_attrs["kl_divergence"]:.4f} | 0 *(by definition)* |
|
||||
| **Refusals** | {trial.user_attrs["refusals"]}/{len(bad_prompts)} | {base_refusals}/{
|
||||
len(bad_prompts)
|
||||
} |
|
||||
| **Refusals** | {trial.user_attrs["refusals"]}/{trial.user_attrs["n_bad_prompts"]} | {
|
||||
trial.user_attrs["base_refusals"]
|
||||
}/{trial.user_attrs["n_bad_prompts"]} |
|
||||
|
||||
-----
|
||||
|
||||
@@ -315,250 +327,276 @@ def get_readme_intro(
|
||||
|
||||
def generate_config_toml(settings: Settings) -> str:
|
||||
"""Serializes the full Settings object to TOML."""
|
||||
|
||||
return tomli_w.dumps(settings.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
def generate_requirements_txt() -> str:
|
||||
"""Collects direct project dependencies as a formatted string."""
|
||||
requirements = get_requirements_dict()
|
||||
sorted_requirements = sorted(
|
||||
[f"{name}=={version}" for name, version in requirements.items()],
|
||||
key=lambda x: x.lower(),
|
||||
)
|
||||
return "\n".join(sorted_requirements) + "\n"
|
||||
|
||||
requirements = [
|
||||
f"{package}=={version}" for package, version in get_requirements_dict().items()
|
||||
]
|
||||
return "\n".join(requirements) + "\n"
|
||||
|
||||
|
||||
def set_seed(seed: int):
|
||||
"""Sets the seed for all RNGs."""
|
||||
|
||||
random.seed(seed)
|
||||
np.random.seed(seed)
|
||||
torch.manual_seed(seed)
|
||||
|
||||
|
||||
def format_hf_link(
|
||||
path: str,
|
||||
commit: str | None = None,
|
||||
is_dataset: bool = False,
|
||||
) -> str:
|
||||
prefix = "datasets/" if is_dataset else ""
|
||||
base_url = f"https://huggingface.co/{prefix}{path}"
|
||||
link = f"[{path}]({base_url})"
|
||||
|
||||
if commit:
|
||||
commit_url = f"{base_url}/commit/{commit}"
|
||||
link += f" (Commit: [`{commit[:7]}`]({commit_url}))"
|
||||
|
||||
return link
|
||||
|
||||
|
||||
def generate_reproduce_readme(
|
||||
settings: Settings,
|
||||
checkpoint_filename: str,
|
||||
trial: Trial,
|
||||
timestamp: str | None = None,
|
||||
base_model_commit: str | None = None,
|
||||
include_system_information: bool,
|
||||
) -> str:
|
||||
"""Generates a README.md for the reproduce/ folder."""
|
||||
torch_version = torch.__version__
|
||||
install_hint = f"pip install torch=={torch_version}"
|
||||
if "+" in torch_version:
|
||||
suffix = torch_version.split("+")[1]
|
||||
if suffix:
|
||||
install_hint += f" --index-url https://download.pytorch.org/whl/{suffix}"
|
||||
"""Generates the contents of a README.md for the reproduce/ folder."""
|
||||
|
||||
heterogeneous_warning = ""
|
||||
if torch.cuda.is_available():
|
||||
count = torch.cuda.device_count()
|
||||
if count > 1:
|
||||
device_names = {torch.cuda.get_device_name(i) for i in range(count)}
|
||||
if len(device_names) > 1:
|
||||
heterogeneous_warning = """
|
||||
|
||||
if include_system_information:
|
||||
if torch.cuda.is_available():
|
||||
count = torch.cuda.device_count()
|
||||
if count > 1:
|
||||
device_names = {torch.cuda.get_device_name(i) for i in range(count)}
|
||||
if len(device_names) > 1:
|
||||
heterogeneous_warning = """
|
||||
> [!WARNING]
|
||||
> **Heterogeneous GPUs Detected!**
|
||||
> This system uses multiple non-identical GPUs. When operations are distributed across different GPUs (e.g. via `device_map='auto'`), non-deterministic behavior can occur. **Reproducibility ***cannot*** be guaranteed in this environment.**
|
||||
> **Heterogeneous GPUs**
|
||||
>
|
||||
> This model was generated using multiple non-identical GPUs. When operations are distributed across different GPUs
|
||||
> (e.g. via `device_map='auto'`), non-deterministic behavior can occur.
|
||||
>
|
||||
> Reproducibility *cannot* be guaranteed in this environment.
|
||||
"""
|
||||
|
||||
version_info = get_heretic_version_info()
|
||||
origin_warning = ""
|
||||
if not version_info.is_standard_pypi:
|
||||
if version_info.origin and version_info.origin.startswith("Git"):
|
||||
repo_info = version_info.origin.split("Git (")[1].strip(")")
|
||||
origin_warning = f"""
|
||||
> [!NOTE]
|
||||
> **Git Installation Detected**
|
||||
> This system installed `heretic-llm` from source repository: `{repo_info}`.
|
||||
> To reproduce these results, you must install Heretic from this exact repository and commit.
|
||||
"""
|
||||
elif version_info.origin == "Local":
|
||||
origin_warning = """
|
||||
> [!WARNING]
|
||||
> **Local Code Detected!**
|
||||
> This system installed `heretic-llm` from a local directory or wheel. Uncommitted or experimental code may have been executed. **Reproducibility ***cannot*** be guaranteed in this environment.**
|
||||
"""
|
||||
cpu = get_cpu_info_dict()
|
||||
python_env = get_python_env_info_dict()
|
||||
|
||||
accelerators = get_accelerator_info_dict()
|
||||
if accelerators["type"] is None:
|
||||
accelerator_report = "**No GPU or other accelerator detected.**"
|
||||
else:
|
||||
origin_warning = """
|
||||
> [!WARNING]
|
||||
> **Non-Standard Installation Detected!**
|
||||
> This system installed `heretic-llm` from an unknown non-standard source. **Reproducibility ***cannot*** be guaranteed in this environment.**
|
||||
"""
|
||||
devices = accelerators["devices"]
|
||||
total_vram = sum(device.get("vram_gb", 0) for device in devices)
|
||||
vram_suffix = f" ({total_vram:.2f} GB total VRAM)" if total_vram > 0 else ""
|
||||
accelerator_lines = [
|
||||
f"- **{accelerators['type']}:** Detected {len(devices)} device(s){vram_suffix}"
|
||||
]
|
||||
|
||||
def format_hf_link(
|
||||
name: str, commit: str | None = None, is_dataset: bool = False
|
||||
) -> str:
|
||||
if Path(name).exists():
|
||||
return f"`{name}` (Local)"
|
||||
if accelerators.get("api_name") and accelerators.get("api_version"):
|
||||
accelerator_lines.append(
|
||||
f" - **{accelerators['api_name']}:** {accelerators['api_version']}"
|
||||
)
|
||||
|
||||
prefix = "datasets/" if is_dataset else ""
|
||||
base_url = f"https://huggingface.co/{prefix}{name}"
|
||||
link = f"[{name}]({base_url})"
|
||||
if commit:
|
||||
commit_url = f"{base_url}/commit/{commit}"
|
||||
link += f" (Commit: [{commit[:7]}]({commit_url}))"
|
||||
return link
|
||||
if accelerators.get("driver_version"):
|
||||
accelerator_lines.append(
|
||||
f" - **Driver Version:** {accelerators['driver_version']}"
|
||||
)
|
||||
|
||||
model_link = format_hf_link(settings.model, base_model_commit)
|
||||
dataset_info = f"""## Dataset Information
|
||||
accelerator_lines.append("- **Devices:**")
|
||||
for i, device in enumerate(devices):
|
||||
vram = f" ({device['vram_gb']:.2f} GB)" if device.get("vram_gb") else ""
|
||||
accelerator_lines.append(
|
||||
f" - **{accelerators['type']} {i}:** {device['name']}{vram}"
|
||||
)
|
||||
accelerator_report = "\n".join(accelerator_lines)
|
||||
|
||||
- **Good Prompts:** {format_hf_link(settings.good_prompts.dataset, settings.good_prompts.commit, is_dataset=True)}
|
||||
- **Bad Prompts:** {format_hf_link(settings.bad_prompts.dataset, settings.bad_prompts.commit, is_dataset=True)}
|
||||
- **Good Evaluation Prompts:** {format_hf_link(settings.good_evaluation_prompts.dataset, settings.good_evaluation_prompts.commit, is_dataset=True)}
|
||||
- **Bad Evaluation Prompts:** {format_hf_link(settings.bad_evaluation_prompts.dataset, settings.bad_evaluation_prompts.commit, is_dataset=True)}"""
|
||||
system_report = f"""## System
|
||||
|
||||
timestamp_str = f"- **Run started at (UTC):** `{timestamp}`" if timestamp else ""
|
||||
|
||||
# System and Accelerator info using structured dictionaries.
|
||||
cpu = get_cpu_info_dict()
|
||||
python_env = get_python_env_info_dict()
|
||||
accelerator = get_accelerator_info_dict()
|
||||
|
||||
# Build System Environment section.
|
||||
system_env_lines = [
|
||||
f"- **OS:** `{platform.platform()}` (`{platform.machine()}`)",
|
||||
f"- **CPU:** `{cpu['brand'] or 'Unknown CPU'}`",
|
||||
f" - **Information:** Family `{cpu['family']}`, Model `{cpu['model']}`, Stepping `{cpu['stepping']}`",
|
||||
]
|
||||
|
||||
system_env_lines.extend(
|
||||
[
|
||||
f"- **Python:** `{python_env['version']}` (`{python_env['implementation']}`, `{python_env['compiler']}`) [`{python_env['environment']}`]",
|
||||
f"- **Heretic:** `v{version_info.version}`"
|
||||
+ (f" (Origin: `{version_info.origin}`)" if version_info.origin else ""),
|
||||
f"- **PyTorch:** `{torch.__version__}`",
|
||||
]
|
||||
)
|
||||
system_environment_report = "\n".join(system_env_lines)
|
||||
|
||||
# Build Accelerators section.
|
||||
if accelerator["type"] is None:
|
||||
accelerator_report = "> [!WARNING]\n> **No GPU or other accelerator detected.**"
|
||||
else:
|
||||
devices = accelerator["devices"]
|
||||
total_vram = sum(d.get("vram_gb", 0) for d in devices)
|
||||
vram_suffix = f" (`{total_vram:.2f} GB` total VRAM)" if total_vram > 0 else ""
|
||||
accelerator_lines = [
|
||||
f"- **{accelerator['type']}:** Detected `{len(devices)}` device(s){vram_suffix}"
|
||||
]
|
||||
|
||||
if accelerator.get("api_name") and accelerator.get("api_version"):
|
||||
accelerator_lines.append(
|
||||
f" - **{accelerator['api_name']}:** `{accelerator['api_version']}`"
|
||||
)
|
||||
|
||||
if accelerator.get("driver_version"):
|
||||
accelerator_lines.append(
|
||||
f" - **Driver Version:** `{accelerator['driver_version']}`"
|
||||
)
|
||||
|
||||
accelerator_lines.append("- **Devices:**")
|
||||
for i, dev in enumerate(devices):
|
||||
vram = f" (`{dev['vram_gb']:.2f} GB`)" if dev.get("vram_gb") else ""
|
||||
accelerator_lines.append(
|
||||
f" - **{accelerator['type']} {i}:** `{dev['name']}`{vram}"
|
||||
)
|
||||
accelerator_report = "\n".join(accelerator_lines)
|
||||
|
||||
return f"""# Reproduction Guide
|
||||
|
||||
This directory contains the necessary information and assets to reproduce the results obtained during this Heretic run.{heterogeneous_warning}{origin_warning}
|
||||
|
||||
## Model Information
|
||||
|
||||
- **Base Model:** {model_link}
|
||||
{timestamp_str}
|
||||
|
||||
{dataset_info}
|
||||
|
||||
## Selected Trial
|
||||
|
||||
- **Trial Number:** `#{trial.user_attrs["index"]}`
|
||||
- **Refusal Count:** `{trial.user_attrs.get("refusals")}/{trial.user_attrs.get("total_refusal_prompts")}`
|
||||
- **KL Divergence:** `{trial.user_attrs.get("kl_divergence", 0):.6f}`
|
||||
|
||||
## System Environment
|
||||
|
||||
{system_environment_report}
|
||||
- **Python:** {python_env["version"]} ({python_env["implementation"]}, {python_env["compiler"]}) [{python_env["environment"]}]
|
||||
- **Operating system:** {platform.platform()} ({platform.machine()})
|
||||
- **CPU:** {cpu["brand"] or "Unknown"}
|
||||
|
||||
### Accelerators
|
||||
|
||||
{accelerator_report}
|
||||
|
||||
## Contents
|
||||
"""
|
||||
system_instructions = (
|
||||
"1. Ensure your system matches the specifications in the **System** section above. "
|
||||
"Exact reproducibility is only guaranteed if all aspects of your system are identical to the one the model was originally generated on.\n"
|
||||
)
|
||||
else:
|
||||
system_report = ""
|
||||
system_instructions = ""
|
||||
|
||||
- **config.toml**: The exact configuration used, including the seed `{settings.seed}`.
|
||||
- **requirements.txt**: The exact versions of all installed Python packages.
|
||||
- **{checkpoint_filename}**: The Optuna study journal containing the history of all trials.
|
||||
- **reproduce.json**: A machine-readable version of this report.
|
||||
- **SHA256SUMS**: Cryptographic hashes for all uploaded weight files (if applicable).
|
||||
version_info = get_heretic_version_info()
|
||||
origin_warning = ""
|
||||
if not version_info.is_standard_pypi:
|
||||
if version_info.origin and version_info.origin.startswith("Git"):
|
||||
repo_info = version_info.origin.split("Git (")[1].rstrip(")")
|
||||
origin_warning = f"""
|
||||
> [!IMPORTANT]
|
||||
> **Git installation**
|
||||
>
|
||||
> This system installed Heretic from a Git repository: {repo_info}
|
||||
>
|
||||
> To reproduce the model, you must install Heretic from this exact repository and commit.
|
||||
"""
|
||||
elif version_info.origin == "Local":
|
||||
origin_warning = """
|
||||
> [!WARNING]
|
||||
> **Local code**
|
||||
>
|
||||
> This system installed Heretic from a local directory or wheel. Uncommitted or experimental code may have been executed.
|
||||
>
|
||||
> Reproducibility *cannot* be guaranteed in this environment.
|
||||
"""
|
||||
else:
|
||||
origin_warning = """
|
||||
> [!WARNING]
|
||||
> **Non-standard installation**
|
||||
>
|
||||
> This system installed Heretic from an unknown non-standard source.
|
||||
>
|
||||
> Reproducibility *cannot* be guaranteed in this environment.
|
||||
"""
|
||||
|
||||
## How to Reproduce
|
||||
pytorch_version = torch.__version__
|
||||
pytorch_install_command = f"pip install torch=={pytorch_version}"
|
||||
if "+" in pytorch_version:
|
||||
suffix = pytorch_version.split("+")[1]
|
||||
if suffix:
|
||||
pytorch_install_command += (
|
||||
f" --index-url https://download.pytorch.org/whl/{suffix}"
|
||||
)
|
||||
|
||||
1. Ensure your hardware and environment match the specifications in the **System Environment** section above.
|
||||
2. Install the exact package versions listed in `requirements.txt`.
|
||||
3. Place the provided `config.toml` in your working directory.
|
||||
4. Run `heretic` without any additional arguments.
|
||||
5. Verify the integrity of the reproduced files by comparing their SHA256 hashes against the manifest in `SHA256SUMS`.
|
||||
return f"""# Reproduction guide
|
||||
|
||||
This directory contains the necessary information and assets to reproduce the results obtained during this Heretic run.{heterogeneous_warning}{origin_warning}
|
||||
|
||||
## Models
|
||||
|
||||
- **Base model:** {format_hf_link(settings.model, settings.model_commit)}
|
||||
|
||||
## Datasets
|
||||
|
||||
- **Good prompts:** {format_hf_link(settings.good_prompts.dataset, settings.good_prompts.commit, is_dataset=True)}
|
||||
- **Bad prompts:** {format_hf_link(settings.bad_prompts.dataset, settings.bad_prompts.commit, is_dataset=True)}
|
||||
- **Good evaluation prompts:** {format_hf_link(settings.good_evaluation_prompts.dataset, settings.good_evaluation_prompts.commit, is_dataset=True)}
|
||||
- **Bad evaluation prompts:** {format_hf_link(settings.bad_evaluation_prompts.dataset, settings.bad_evaluation_prompts.commit, is_dataset=True)}
|
||||
|
||||
## Selected trial
|
||||
|
||||
- **Trial number:** {trial.user_attrs["index"]}
|
||||
- **KL divergence:** {trial.user_attrs["kl_divergence"]:.6f}
|
||||
- **Refusals:** {trial.user_attrs["refusals"]}/{trial.user_attrs["n_bad_prompts"]}
|
||||
|
||||
{system_report}## Environment
|
||||
|
||||
- **Heretic:** v{version_info.version}{f" (Origin: {version_info.origin})" if version_info.origin else ""}
|
||||
- **PyTorch:** {pytorch_version}
|
||||
- **Other dependencies:** See [`requirements.txt`](requirements.txt).
|
||||
|
||||
## Contents of this directory
|
||||
|
||||
- [`requirements.txt`](requirements.txt): The exact versions of all Python packages.
|
||||
- [`config.toml`](config.toml): The exact configuration used, including the RNG seed.
|
||||
- [`{checkpoint_filename}`]({checkpoint_filename}): The Optuna study journal containing the history of all trials.
|
||||
- [`SHA256SUMS`](SHA256SUMS): Cryptographic hashes for all weight files.
|
||||
- [`reproduce.json`](reproduce.json): A machine-readable file containing all reproducibility information.
|
||||
|
||||
## How to reproduce
|
||||
|
||||
{system_instructions}1. Install the exact version of Heretic indicated in the **Environment** section above, from its original source.
|
||||
1. Install the packages listed in `requirements.txt`: `pip install -r requirements.txt`
|
||||
1. Install the correct version of PyTorch: `{pytorch_install_command}`
|
||||
1. Place the provided `config.toml` in your working directory.
|
||||
1. Run Heretic without any additional arguments: `heretic`
|
||||
1. Wait for the run to finish, then select trial **{trial.user_attrs["index"]}** and export the model.
|
||||
1. Verify that the weight files have been exactly reproduced by comparing their SHA-256 hashes against those in `SHA256SUMS`: `sha256sum -c SHA256SUMS` (or look at the hashes online if you uploaded to Hugging Face)
|
||||
|
||||
> [!TIP]
|
||||
> To use the included Optuna study journal `{checkpoint_filename}`, place it in a `checkpoints/` directory before running `heretic` on the same model.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Make sure to install correct PyTorch version from: `{install_hint}`
|
||||
> To use the included Optuna study journal `{checkpoint_filename}`, place it in the checkpoints directory (usually `checkpoints/`) before running Heretic.
|
||||
>
|
||||
> This allows you to export other models from the Pareto front, or to run additional trials without having to re-run the stored trials.
|
||||
"""
|
||||
|
||||
|
||||
def generate_reproduce_json(
|
||||
settings: Settings,
|
||||
trial: Trial,
|
||||
timestamp: str | None = None,
|
||||
base_model_commit: str | None = None,
|
||||
uploaded_model_hashes: dict[str, str] | None = None,
|
||||
timestamp: str,
|
||||
uploaded_model_hashes: dict[str, str],
|
||||
include_system_information: bool,
|
||||
) -> str:
|
||||
"""Generates a reproduce.json file for the reproduce/ folder."""
|
||||
"""Generates the contents of a reproduce.json file for the reproduce/ folder."""
|
||||
|
||||
version_info = get_heretic_version_info()
|
||||
|
||||
data = {
|
||||
"base_model": {
|
||||
"id": settings.model,
|
||||
"commit_hash": base_model_commit,
|
||||
},
|
||||
"system": {
|
||||
"os": {"platform": platform.platform(), "machine": platform.machine()},
|
||||
"cpu": get_cpu_info_dict(),
|
||||
"python": get_python_env_info_dict(),
|
||||
"version": "1", # Version number of the reproduce.json file format, to allow for future changes.
|
||||
"timestamp": timestamp,
|
||||
"system": None, # Defined here to preserve insertion order.
|
||||
"environment": {
|
||||
"heretic": {
|
||||
"version": version_info.version,
|
||||
"is_standard_pypi": version_info.is_standard_pypi,
|
||||
"metadata": version_info.metadata,
|
||||
},
|
||||
"pytorch_version": torch.__version__,
|
||||
"accelerator": get_accelerator_info_dict(),
|
||||
"requirements": get_requirements_dict(),
|
||||
},
|
||||
"requirements": get_requirements_dict(),
|
||||
"settings": settings.model_dump(exclude_none=True),
|
||||
"trial": {
|
||||
"direction_index": trial.user_attrs.get("direction_index"),
|
||||
"parameters": trial.user_attrs.get("parameters"),
|
||||
"metrics": {
|
||||
"refusals": trial.user_attrs.get("refusals"),
|
||||
"total_refusal_prompts": trial.user_attrs.get("total_refusal_prompts"),
|
||||
"kl_divergence": trial.user_attrs.get("kl_divergence"),
|
||||
},
|
||||
"settings": settings.model_dump(),
|
||||
"parameters": {
|
||||
"direction_index": trial.user_attrs["direction_index"],
|
||||
"abliteration_parameters": trial.user_attrs["parameters"],
|
||||
},
|
||||
"timestamp": timestamp,
|
||||
"uploaded_model_hashes": uploaded_model_hashes or {},
|
||||
"metrics": {
|
||||
"kl_divergence": trial.user_attrs["kl_divergence"],
|
||||
"refusals": trial.user_attrs["refusals"],
|
||||
"base_refusals": trial.user_attrs["base_refusals"],
|
||||
"n_bad_prompts": trial.user_attrs["n_bad_prompts"],
|
||||
},
|
||||
"hashes": uploaded_model_hashes,
|
||||
}
|
||||
|
||||
if include_system_information:
|
||||
data["system"] = {
|
||||
"python": get_python_env_info_dict(),
|
||||
"os": {
|
||||
"platform": platform.platform(),
|
||||
"machine": platform.machine(),
|
||||
},
|
||||
"cpu": get_cpu_info_dict(),
|
||||
"accelerators": get_accelerator_info_dict(),
|
||||
}
|
||||
else:
|
||||
del data["system"]
|
||||
|
||||
return json.dumps(data, indent=4)
|
||||
|
||||
|
||||
def generate_sha256sums(hashes: dict[str, str]) -> str:
|
||||
"""Generates a GNU Coreutils compatible SHA256SUMS file content."""
|
||||
"""Generates GNU Coreutils compatible SHA256SUMS file content."""
|
||||
|
||||
lines = []
|
||||
|
||||
for filename, sha256 in sorted(hashes.items()):
|
||||
# Use '*' to indicate binary mode for model weights.
|
||||
lines.append(f"{sha256} *{filename}")
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
@@ -567,13 +605,17 @@ def create_reproduce_folder(
|
||||
settings: Settings,
|
||||
checkpoint_path: str | Path,
|
||||
trial: Trial,
|
||||
uploaded_model_hashes: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
uploaded_model_hashes: dict[str, str],
|
||||
include_system_information: bool,
|
||||
):
|
||||
reproduce_dir = path / "reproduce"
|
||||
reproduce_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
checkpoint_filename = Path(checkpoint_path).name
|
||||
|
||||
# Fetch commit hash for the base model.
|
||||
settings.model_commit = huggingface_hub.model_info(settings.model).sha
|
||||
|
||||
# Fetch commit hashes for all HF datasets to ensure reproducibility.
|
||||
for spec in [
|
||||
settings.good_prompts,
|
||||
@@ -581,50 +623,46 @@ def create_reproduce_folder(
|
||||
settings.good_evaluation_prompts,
|
||||
settings.bad_evaluation_prompts,
|
||||
]:
|
||||
if not Path(spec.dataset).exists():
|
||||
# Fail if the dataset is missing or unreachable.
|
||||
spec.commit = huggingface_hub.dataset_info(spec.dataset).sha
|
||||
|
||||
# Fetch commit hash for the base model if it's on HF.
|
||||
base_model_commit = None
|
||||
if not Path(settings.model).exists():
|
||||
try:
|
||||
base_model_commit = huggingface_hub.model_info(settings.model).sha
|
||||
except Exception:
|
||||
pass
|
||||
spec.commit = huggingface_hub.dataset_info(spec.dataset).sha
|
||||
|
||||
# Strip microseconds and timezone for a clean format.
|
||||
timestamp = (
|
||||
datetime.now(timezone.utc).replace(microsecond=0, tzinfo=None).isoformat()
|
||||
)
|
||||
|
||||
(reproduce_dir / "config.toml").write_text(
|
||||
generate_config_toml(settings), encoding="utf-8"
|
||||
)
|
||||
(reproduce_dir / "requirements.txt").write_text(
|
||||
generate_requirements_txt(), encoding="utf-8"
|
||||
)
|
||||
(reproduce_dir / "README.md").write_text(
|
||||
generate_reproduce_readme(
|
||||
settings,
|
||||
checkpoint_filename,
|
||||
trial,
|
||||
timestamp=timestamp,
|
||||
base_model_commit=base_model_commit,
|
||||
),
|
||||
generate_requirements_txt(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
(reproduce_dir / "config.toml").write_text(
|
||||
generate_config_toml(settings),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
if uploaded_model_hashes:
|
||||
(reproduce_dir / "SHA256SUMS").write_text(
|
||||
generate_sha256sums(uploaded_model_hashes), encoding="utf-8"
|
||||
generate_sha256sums(uploaded_model_hashes),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
(reproduce_dir / "reproduce.json").write_text(
|
||||
generate_reproduce_json(
|
||||
settings,
|
||||
trial,
|
||||
timestamp=timestamp,
|
||||
base_model_commit=base_model_commit,
|
||||
uploaded_model_hashes=uploaded_model_hashes,
|
||||
include_system_information=include_system_information,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
(reproduce_dir / "README.md").write_text(
|
||||
generate_reproduce_readme(
|
||||
settings,
|
||||
checkpoint_filename,
|
||||
trial,
|
||||
include_system_information=include_system_information,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -641,22 +679,25 @@ def upload_reproduce_folder(
|
||||
token: str,
|
||||
checkpoint_path: str | Path,
|
||||
trial: Trial,
|
||||
) -> None:
|
||||
include_system_information: bool,
|
||||
):
|
||||
api = huggingface_hub.HfApi()
|
||||
info = api.model_info(repo_id=repo_id, files_metadata=True, token=token)
|
||||
|
||||
if not info.siblings:
|
||||
raise RuntimeError("Could not fetch uploaded model hashes.")
|
||||
|
||||
# For weights, we only care about safetensors.
|
||||
weight_extensions = (".safetensors",)
|
||||
|
||||
uploaded_model_hashes = {}
|
||||
try:
|
||||
api = huggingface_hub.HfApi()
|
||||
info = api.model_info(repo_id=repo_id, files_metadata=True, token=token)
|
||||
# For weights, we only care about safetensors.
|
||||
weight_extensions = (".safetensors",)
|
||||
if info.siblings is not None:
|
||||
for file in info.siblings:
|
||||
if file.rfilename.endswith(weight_extensions):
|
||||
sha256 = getattr(file, "lfs", {}).get("sha256")
|
||||
if sha256:
|
||||
uploaded_model_hashes[file.rfilename] = sha256
|
||||
except Exception as e:
|
||||
# Fail if integrity checks cannot be completed.
|
||||
raise RuntimeError(f"Could not fetch uploaded model hashes: {e}") from e
|
||||
|
||||
for file in info.siblings:
|
||||
if file.rfilename.endswith(weight_extensions):
|
||||
sha256 = getattr(file, "lfs", {}).get("sha256")
|
||||
if not sha256:
|
||||
raise RuntimeError("Could not fetch uploaded model hashes.")
|
||||
uploaded_model_hashes[file.rfilename] = sha256
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
tmp_path = Path(tmpdir)
|
||||
@@ -666,6 +707,7 @@ def upload_reproduce_folder(
|
||||
checkpoint_path=checkpoint_path,
|
||||
trial=trial,
|
||||
uploaded_model_hashes=uploaded_model_hashes,
|
||||
include_system_information=include_system_information,
|
||||
)
|
||||
|
||||
reproduce_dir = tmp_path / "reproduce"
|
||||
|
||||
@@ -8,7 +8,7 @@ resolution-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-04-14T22:48:57.86057843Z"
|
||||
exclude-newer = "2026-04-28T12:47:55.130721483Z"
|
||||
exclude-newer-span = "P7D"
|
||||
|
||||
[[package]]
|
||||
@@ -931,7 +931,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "heretic-llm"
|
||||
version = "1.2.0"
|
||||
version = "1.3.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "accelerate" },
|
||||
@@ -983,14 +983,14 @@ requires-dist = [
|
||||
{ name = "huggingface-hub", specifier = "~=1.7" },
|
||||
{ name = "imageio", marker = "extra == 'research'", specifier = "~=2.37" },
|
||||
{ name = "immutabledict", specifier = "~=4.3" },
|
||||
{ name = "kernels", specifier = "~=0.12" },
|
||||
{ name = "kernels", specifier = "~=0.13" },
|
||||
{ name = "langdetect", specifier = "~=1.0" },
|
||||
{ name = "lm-eval", extras = ["hf"], specifier = "~=0.4" },
|
||||
{ name = "matplotlib", marker = "extra == 'research'", specifier = "~=3.10" },
|
||||
{ name = "numpy", specifier = "~=2.2" },
|
||||
{ name = "optuna", specifier = "~=4.7" },
|
||||
{ name = "pacmap", marker = "extra == 'research'", specifier = "~=0.8" },
|
||||
{ name = "peft", specifier = "~=0.18" },
|
||||
{ name = "peft", specifier = "~=0.19" },
|
||||
{ name = "psutil", specifier = "~=7.2" },
|
||||
{ name = "py-cpuinfo", specifier = "~=9.0" },
|
||||
{ name = "pydantic-settings", specifier = "~=2.13" },
|
||||
@@ -999,7 +999,7 @@ requires-dist = [
|
||||
{ name = "scikit-learn", marker = "extra == 'research'", specifier = "~=1.7" },
|
||||
{ name = "tomli-w", specifier = "~=1.2" },
|
||||
{ name = "tqdm", specifier = "~=4.67" },
|
||||
{ name = "transformers", specifier = "~=5.3" },
|
||||
{ name = "transformers", specifier = "~=5.6" },
|
||||
]
|
||||
provides-extras = ["research"]
|
||||
|
||||
@@ -1188,17 +1188,18 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "kernels"
|
||||
version = "0.12.3"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
{ name = "tomlkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/84/9f68f355f6ce99e977872021fbdbafadcf2820f51d3f7bd697ec3801cb7a/kernels-0.12.3.tar.gz", hash = "sha256:87e29716578e7e71dc5a7578e0132bfdae305bedaeb602698f87c88ca6c60e32", size = 57407, upload-time = "2026-03-20T10:20:42.166Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3e/0d/e9c158c527a7b51382fe816a7b7e60caae17ff1153640c1803211a067c99/kernels-0.13.0.tar.gz", hash = "sha256:bf7908206009bff0017d09b87f0f6b5934a1a20520562caf1cbb06cab36418cc", size = 74755, upload-time = "2026-04-10T14:30:45.356Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/3e/778e4a86830e9139df2d16d86c4488fce426ec19daa83cbd2854ef389030/kernels-0.12.3-py3-none-any.whl", hash = "sha256:5d1d33fcb774e03bb7f0688ac24d91ef6b963692f80f0a85ddd2286e69f3cf2f", size = 55501, upload-time = "2026-03-20T10:20:40.643Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/45/2cb29e965c199ab01151fee24cbb57b23550c9e6bc897ca242b1e4b8c4bf/kernels-0.13.0-py3-none-any.whl", hash = "sha256:5d857ee4e06dc7496bcd59c4756e84eb71c019b34524dea58ccb0eaaae3bb6df", size = 69177, upload-time = "2026-04-10T14:30:43.551Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1508,14 +1509,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mako"
|
||||
version = "1.3.10"
|
||||
version = "1.3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/8a/805404d0c0b9f3d7a326475ca008db57aea9c5c9f2e1e39ed0faa335571c/mako-1.3.11.tar.gz", hash = "sha256:071eb4ab4c5010443152255d77db7faa6ce5916f35226eb02dc34479b6858069", size = 399811, upload-time = "2026-04-14T20:19:51.493Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/a5/19d7aaa7e433713ffe881df33705925a196afb9532efc8475d26593921a6/mako-1.3.11-py3-none-any.whl", hash = "sha256:e372c6e333cf004aa736a15f425087ec977e1fcbd2966aae7f17c8dc1da27a77", size = 78503, upload-time = "2026-04-14T20:19:53.233Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2365,7 +2366,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "peft"
|
||||
version = "0.18.0"
|
||||
version = "0.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "accelerate" },
|
||||
@@ -2380,9 +2381,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "transformers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4b/0c/f2938db546ac7fc961ab5917cd50fcf5d0d70b406de93e3faccaa504e152/peft-0.18.0.tar.gz", hash = "sha256:c81c80b2056ab40c23d58ef25f74daab417ac653970718589a11a8af28218588", size = 634141, upload-time = "2025-11-13T11:13:06.603Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/86/cf/037f1e3d5186496c05513a6754639e2dab3038a05f384284d49a9bd06a2d/peft-0.19.1.tar.gz", hash = "sha256:0d97542fe96dcdaa20d3b81c06f26f988618f416a73544ab23c3618ccb674a40", size = 763738, upload-time = "2026-04-16T15:46:45.105Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/55/481bf25613d40ef53534f664deba7b138fe566356b6ca10304e2b3b2529c/peft-0.18.0-py3-none-any.whl", hash = "sha256:624f69ca6393b765ccc6734adda7ca57d80b238f0900a42c357d8b67a03d62ff", size = 556427, upload-time = "2025-11-13T11:13:03.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/b6/f54d676ed93cc2dd2234c3b172ea9c8c3d7d29361e66b1b23dec57a67465/peft-0.19.1-py3-none-any.whl", hash = "sha256:2113f72a81621b5913ef28f9022204c742df111890c5f49d812716a4a301e356", size = 680692, upload-time = "2026-04-16T15:46:42.886Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3696,6 +3697,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomlkit"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "torch"
|
||||
version = "2.9.1"
|
||||
@@ -3771,7 +3781,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "transformers"
|
||||
version = "5.3.0"
|
||||
version = "5.6.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
@@ -3785,9 +3795,9 @@ dependencies = [
|
||||
{ name = "tqdm" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/e9/c6c80a07690142a7d05444271f47b9f3c8aac7dea01d52e1137ee480ad78/transformers-5.6.2.tar.gz", hash = "sha256:e657134c3e5a6bc00a3c35f4e2674bb51adfcd89898495b788a18552bac2b91a", size = 8311867, upload-time = "2026-04-23T18:33:29.332Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/88/ae8320064e32679a5429a2c9ebbc05c2bf32cefb6e076f9b07f6d685a9b4/transformers-5.3.0-py3-none-any.whl", hash = "sha256:50ac8c89c3c7033444fb3f9f53138096b997ebb70d4b5e50a2e810bf12d3d29a", size = 10661827, upload-time = "2026-03-04T17:41:42.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/95/0b0218149b0d6f14df35f5b8f676fa83df4f19ed253c3cc447107ef86eca/transformers-5.6.2-py3-none-any.whl", hash = "sha256:f8d3a1bb96778fed9b8aabfd0dd6e19843e4b0f2bb6b59f32b8a92051b0f348f", size = 10364898, upload-time = "2026-04-23T18:33:26.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user