mirror of
https://github.com/p-e-w/heretic.git
synced 2026-10-02 08:51:27 -07:00
feat: headless operation + end-to-end tests (#392)
* fix: remove notebook input shims Closes #280 * feat: support headless operation (no interactive input) * fix: prevent infinite loops * feat: add end-to-end tests * ci: run tests in CI * ci: fix test output ordering * fix: replace home-cooked `set_seed` function with Transformers builtin * feat: print PyTorch config when running tests * feat: print additional information * experiment: try to standardize test environment * fix: revert environment changes * feat: support multiple valid hashes for each output file * feat: add test output hashes for CI * feat: add test output hashes for CI (alternative environment) * feat: add hashes for Windows (#394) * fix: Hash on windows * trigger ci * fix: prefer .yaml (used widely than .toml for model configs) * use removeprefix * docs: restore commet * use removeprefix again * tests: Add windows hash files for all test models * trigger ci * fix: minor cleanup * clean merge mismatch * remove unnecessary CRLF replace, now that we support more SUMS files * fix: use binary mode for hashes everywhere --------- Co-authored-by: Vinay Umrethe <umrethevinay@gmail.com>
This commit is contained in:
co-authored by
Vinay Umrethe
parent
3f68a0d4e5
commit
0146b2760f
+68
-10
@@ -4,7 +4,12 @@
|
||||
from enum import Enum
|
||||
from typing import Dict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
Field,
|
||||
NonNegativeInt,
|
||||
PositiveInt,
|
||||
)
|
||||
from pydantic_settings import (
|
||||
BaseSettings,
|
||||
CliSettingsSource,
|
||||
@@ -181,12 +186,12 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
batch_size: int = Field(
|
||||
batch_size: NonNegativeInt = Field(
|
||||
default=0, # auto
|
||||
description="Number of input sequences to process in parallel (0 = auto).",
|
||||
)
|
||||
|
||||
max_batch_size: int = Field(
|
||||
max_batch_size: PositiveInt = 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,
|
||||
@@ -194,7 +199,7 @@ class Settings(BaseSettings):
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
max_response_length: int = Field(
|
||||
max_response_length: PositiveInt = Field(
|
||||
default=100,
|
||||
description="Maximum number of tokens to generate for each response.",
|
||||
)
|
||||
@@ -247,6 +252,12 @@ class Settings(BaseSettings):
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
print_debug_information: bool = Field(
|
||||
default=False,
|
||||
description="Whether to print additional information that can help with debugging.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
print_residual_geometry: bool = Field(
|
||||
default=False,
|
||||
description="Whether to print detailed information about residuals and refusal directions.",
|
||||
@@ -311,7 +322,7 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
full_normalization_lora_rank: int = Field(
|
||||
full_normalization_lora_rank: PositiveInt = Field(
|
||||
default=3,
|
||||
description=(
|
||||
'The rank of the LoRA adapter to use when "full" row normalization is used. '
|
||||
@@ -332,12 +343,12 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
n_trials: int = Field(
|
||||
n_trials: PositiveInt = Field(
|
||||
default=200,
|
||||
description="Number of abliteration trials to run during optimization.",
|
||||
)
|
||||
|
||||
n_startup_trials: int = Field(
|
||||
n_startup_trials: NonNegativeInt = Field(
|
||||
default=60,
|
||||
description="Number of trials that use random sampling for the purpose of exploration.",
|
||||
)
|
||||
@@ -418,14 +429,61 @@ class Settings(BaseSettings):
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
max_shard_size: PositiveInt | str = Field(
|
||||
default="5GB",
|
||||
description="Maximum size for individual safetensors files generated when exporting a model.",
|
||||
)
|
||||
|
||||
export_strategy: ExportStrategy | None = Field(
|
||||
default=None,
|
||||
description='How to export the model: "merge", "adapter", or unset to prompt the user.',
|
||||
)
|
||||
|
||||
max_shard_size: int | str = Field(
|
||||
default="5GB",
|
||||
description="Maximum size for individual safetensors files generated when exporting a model.",
|
||||
checkpoint_action: str | None = Field(
|
||||
default=None,
|
||||
description='Action to take in case a checkpoint exists: "continue", "restart", or unset to prompt the user.',
|
||||
)
|
||||
|
||||
trial_index: NonNegativeInt | None = Field(
|
||||
default=None,
|
||||
description="Index (in the sorted Pareto front) of the trial to use, or unset to prompt the user.",
|
||||
)
|
||||
|
||||
n_additional_trials: PositiveInt | None = Field(
|
||||
default=None,
|
||||
description="Number of additional trials to run, or unset to prompt the user.",
|
||||
)
|
||||
|
||||
model_action: str | None = Field(
|
||||
default=None,
|
||||
description='Action to take with the decensored model: "save", "upload", or unset to prompt the user.',
|
||||
)
|
||||
|
||||
save_directory: str | None = Field(
|
||||
default=None,
|
||||
description="Directory to save the model to, or unset to prompt the user.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
upload_repo_id: str | None = Field(
|
||||
default=None,
|
||||
description="Name of the Hugging Face repository to upload the model to, or unset to prompt the user.",
|
||||
exclude=True,
|
||||
)
|
||||
|
||||
upload_repo_private: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether the Hugging Face repository to upload the model to should be private, or unset to prompt the user.",
|
||||
)
|
||||
|
||||
upload_reproducibility_information: str | None = Field(
|
||||
default=None,
|
||||
description='Which reproducibility information to add to the Hugging Face repository: "full", "basic", "none", or unset to prompt the user.',
|
||||
)
|
||||
|
||||
ignore_mismatches: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to attempt to reproduce the model even if there are environment mismatches, or unset to prompt the user.",
|
||||
)
|
||||
|
||||
refusal_markers: list[str] = Field(
|
||||
|
||||
+233
-129
@@ -80,6 +80,7 @@ from .reproduce import (
|
||||
)
|
||||
from .system import empty_cache, get_accelerator_info
|
||||
from .utils import (
|
||||
ask_if_unset,
|
||||
format_duration,
|
||||
format_exception,
|
||||
get_file_sha256,
|
||||
@@ -89,11 +90,6 @@ from .utils import (
|
||||
load_prompts,
|
||||
print,
|
||||
print_memory_usage,
|
||||
prompt_password,
|
||||
prompt_path,
|
||||
prompt_select,
|
||||
prompt_text,
|
||||
set_seed,
|
||||
upload_reproduce_folder,
|
||||
)
|
||||
|
||||
@@ -108,10 +104,10 @@ def obtain_export_strategy(
|
||||
Returns an export strategy, or None if cancelled.
|
||||
"""
|
||||
|
||||
if settings.export_strategy is not None:
|
||||
return settings.export_strategy
|
||||
|
||||
if settings.quantization == QuantizationMethod.BNB_4BIT:
|
||||
if (
|
||||
settings.quantization == QuantizationMethod.BNB_4BIT
|
||||
and settings.export_strategy is None
|
||||
):
|
||||
print()
|
||||
print(
|
||||
"The model was loaded with quantization. Merging requires reloading the base model."
|
||||
@@ -155,27 +151,29 @@ def obtain_export_strategy(
|
||||
|
||||
print()
|
||||
|
||||
strategy = prompt_select(
|
||||
"How do you want to export the model?",
|
||||
choices=[
|
||||
Choice(
|
||||
title="Merge the abliteration LoRA and export the full model"
|
||||
+ (
|
||||
""
|
||||
if settings.quantization == QuantizationMethod.NONE
|
||||
else " (requires sufficient RAM)"
|
||||
return ask_if_unset(
|
||||
settings.export_strategy,
|
||||
questionary.select(
|
||||
"How do you want to export the model?",
|
||||
choices=[
|
||||
Choice(
|
||||
title="Merge the abliteration LoRA and export the full model"
|
||||
+ (
|
||||
""
|
||||
if settings.quantization == QuantizationMethod.NONE
|
||||
else " (requires sufficient RAM)"
|
||||
),
|
||||
value=ExportStrategy.MERGE,
|
||||
),
|
||||
value=ExportStrategy.MERGE,
|
||||
),
|
||||
Choice(
|
||||
title="Export the abliteration LoRA only (can be merged later)",
|
||||
value=ExportStrategy.ADAPTER,
|
||||
),
|
||||
],
|
||||
Choice(
|
||||
title="Export the abliteration LoRA only (can be merged later)",
|
||||
value=ExportStrategy.ADAPTER,
|
||||
),
|
||||
],
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
),
|
||||
)
|
||||
|
||||
return strategy
|
||||
|
||||
|
||||
def run():
|
||||
# Enable expandable segments to reduce memory fragmentation on multi-GPU setups.
|
||||
@@ -254,7 +252,7 @@ def run():
|
||||
)
|
||||
return
|
||||
|
||||
if not check_environment(reproduction_information):
|
||||
if not check_environment(settings, reproduction_information):
|
||||
return
|
||||
|
||||
print()
|
||||
@@ -266,10 +264,22 @@ def run():
|
||||
if settings.seed is None:
|
||||
settings.seed = random.randint(0, 2**32 - 1)
|
||||
|
||||
set_seed(settings.seed)
|
||||
transformers.set_seed(settings.seed)
|
||||
|
||||
print(get_accelerator_info())
|
||||
|
||||
if settings.print_debug_information:
|
||||
print()
|
||||
print(torch.__config__.show().strip())
|
||||
print()
|
||||
print(
|
||||
f"torch.backends.mkldnn.enabled = [bold]{torch.backends.mkldnn.enabled}[/]"
|
||||
)
|
||||
print(f"torch.get_num_threads() = [bold]{torch.get_num_threads()}[/]")
|
||||
print(
|
||||
f"torch.get_num_interop_threads() = [bold]{torch.get_num_interop_threads()}[/]"
|
||||
)
|
||||
|
||||
# We don't need gradients as we only do inference.
|
||||
torch.set_grad_enabled(False)
|
||||
|
||||
@@ -320,15 +330,17 @@ def run():
|
||||
choices = []
|
||||
|
||||
if existing_study.user_attrs["finished"]:
|
||||
print()
|
||||
print(
|
||||
(
|
||||
"[green]You have already processed this model.[/] "
|
||||
"You can show the results from the previous run, allowing you to export models or to run additional trials. "
|
||||
"Alternatively, you can ignore the previous run and start from scratch. "
|
||||
"This will delete the checkpoint file and all results from the previous run."
|
||||
if settings.checkpoint_action is None:
|
||||
print()
|
||||
print(
|
||||
(
|
||||
"[green]You have already processed this model.[/] "
|
||||
"You can show the results from the previous run, allowing you to export models or to run additional trials. "
|
||||
"Alternatively, you can ignore the previous run and start from scratch. "
|
||||
"This will delete the checkpoint file and all results from the previous run."
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
choices.append(
|
||||
Choice(
|
||||
title="Show the results from the previous run",
|
||||
@@ -336,15 +348,17 @@ def run():
|
||||
)
|
||||
)
|
||||
else:
|
||||
print()
|
||||
print(
|
||||
(
|
||||
"[yellow]You have already processed this model, but the run was interrupted.[/] "
|
||||
"You can continue the previous run from where it stopped. This will override any specified settings. "
|
||||
"Alternatively, you can ignore the previous run and start from scratch. "
|
||||
"This will delete the checkpoint file and all results from the previous run."
|
||||
if settings.checkpoint_action is None:
|
||||
print()
|
||||
print(
|
||||
(
|
||||
"[yellow]You have already processed this model, but the run was interrupted.[/] "
|
||||
"You can continue the previous run from where it stopped. This will override any specified settings. "
|
||||
"Alternatively, you can ignore the previous run and start from scratch. "
|
||||
"This will delete the checkpoint file and all results from the previous run."
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
choices.append(
|
||||
Choice(
|
||||
title="Continue the previous run",
|
||||
@@ -366,19 +380,29 @@ def run():
|
||||
)
|
||||
)
|
||||
|
||||
print()
|
||||
choice = prompt_select("How would you like to proceed?", choices)
|
||||
if settings.checkpoint_action is None:
|
||||
print()
|
||||
|
||||
if choice == "continue":
|
||||
action = ask_if_unset(
|
||||
settings.checkpoint_action,
|
||||
questionary.select(
|
||||
"How would you like to proceed?",
|
||||
choices=choices,
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
),
|
||||
)
|
||||
|
||||
if action is None or action == "":
|
||||
return
|
||||
|
||||
if action == "continue":
|
||||
settings = Settings.model_validate_json(
|
||||
existing_study.user_attrs["settings"]
|
||||
)
|
||||
elif choice == "restart":
|
||||
elif action == "restart":
|
||||
os.unlink(study_checkpoint_file)
|
||||
backend = JournalFileBackend(study_checkpoint_file, lock_obj=lock_obj)
|
||||
storage = JournalStorage(backend)
|
||||
elif choice is None or choice == "":
|
||||
return
|
||||
|
||||
model = Model(settings)
|
||||
print()
|
||||
@@ -619,7 +643,7 @@ def run():
|
||||
min_weight_distance = trial.suggest_float(
|
||||
f"{component}.min_weight_distance",
|
||||
1.0,
|
||||
0.6 * last_layer_index,
|
||||
max(0.6 * last_layer_index, 1.0),
|
||||
)
|
||||
|
||||
parameters[component] = AbliterationParameters(
|
||||
@@ -709,7 +733,9 @@ def run():
|
||||
if len(study.trials) == settings.n_trials:
|
||||
study.set_user_attr("finished", True)
|
||||
|
||||
while True:
|
||||
trial_loop_active = True
|
||||
|
||||
while trial_loop_active:
|
||||
if not reproduction_mode:
|
||||
# If no trials at all have been evaluated, the study must have been stopped
|
||||
# by pressing Ctrl+C while the first trial was running. In this case, we just
|
||||
@@ -766,18 +792,24 @@ def run():
|
||||
|
||||
print()
|
||||
print("[bold green]Optimization finished![/]")
|
||||
print()
|
||||
print(
|
||||
(
|
||||
"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, "
|
||||
"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.[/]"
|
||||
)
|
||||
)
|
||||
|
||||
while True:
|
||||
if settings.trial_index is None:
|
||||
print()
|
||||
print(
|
||||
(
|
||||
"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, "
|
||||
"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.[/]"
|
||||
)
|
||||
)
|
||||
|
||||
while trial_loop_active:
|
||||
# Ensure a predefined trial is only processed once.
|
||||
if settings.trial_index is not None:
|
||||
trial_loop_active = False
|
||||
|
||||
if reproduction_mode:
|
||||
parameters = reproduction_information["parameters"]
|
||||
metrics = reproduction_information["metrics"]
|
||||
@@ -797,8 +829,19 @@ def run():
|
||||
print()
|
||||
print("Restoring model from reproduction information...")
|
||||
else:
|
||||
print()
|
||||
trial = prompt_select("Which trial do you want to use?", choices)
|
||||
if settings.trial_index is None:
|
||||
print()
|
||||
|
||||
trial = ask_if_unset(
|
||||
None
|
||||
if settings.trial_index is None
|
||||
else best_trials[settings.trial_index],
|
||||
questionary.select(
|
||||
"Which trial do you want to use?",
|
||||
choices=choices,
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
),
|
||||
)
|
||||
|
||||
if trial is None or trial == "":
|
||||
return
|
||||
@@ -806,8 +849,11 @@ def run():
|
||||
if trial == "continue":
|
||||
while True:
|
||||
try:
|
||||
n_additional_trials = prompt_text(
|
||||
"How many additional trials do you want to run?"
|
||||
n_additional_trials = ask_if_unset(
|
||||
settings.n_additional_trials,
|
||||
questionary.text(
|
||||
"How many additional trials do you want to run?"
|
||||
),
|
||||
)
|
||||
if n_additional_trials is None or n_additional_trials == "":
|
||||
n_additional_trials = 0
|
||||
@@ -866,22 +912,46 @@ def run():
|
||||
|
||||
reset_trial_model()
|
||||
|
||||
while True:
|
||||
print()
|
||||
action = prompt_select(
|
||||
"What do you want to do with the decensored model?",
|
||||
[
|
||||
"Save the model to a local folder",
|
||||
"Upload the model to Hugging Face",
|
||||
"Chat with the model",
|
||||
"Benchmark the model",
|
||||
Choice(
|
||||
title="Exit program"
|
||||
if reproduction_mode
|
||||
else "Return to the trial selection menu",
|
||||
value="",
|
||||
),
|
||||
],
|
||||
action_loop_active = True
|
||||
|
||||
while action_loop_active:
|
||||
# Ensure a predefined action is only executed once.
|
||||
if settings.model_action is not None:
|
||||
action_loop_active = False
|
||||
|
||||
if settings.model_action is None:
|
||||
print()
|
||||
|
||||
action = ask_if_unset(
|
||||
settings.model_action,
|
||||
questionary.select(
|
||||
"What do you want to do with the decensored model?",
|
||||
choices=[
|
||||
Choice(
|
||||
title="Save the model to a local folder",
|
||||
value="save",
|
||||
),
|
||||
Choice(
|
||||
title="Upload the model to Hugging Face",
|
||||
value="upload",
|
||||
),
|
||||
Choice(
|
||||
title="Chat with the model",
|
||||
value="chat",
|
||||
),
|
||||
Choice(
|
||||
title="Benchmark the model",
|
||||
value="benchmark",
|
||||
),
|
||||
Choice(
|
||||
title="Exit program"
|
||||
if reproduction_mode
|
||||
else "Return to the trial selection menu",
|
||||
value="",
|
||||
),
|
||||
],
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
),
|
||||
)
|
||||
|
||||
if action is None or action == "":
|
||||
@@ -895,8 +965,14 @@ def run():
|
||||
# the optimized model.
|
||||
try:
|
||||
match action:
|
||||
case "Save the model to a local folder":
|
||||
save_directory = prompt_path("Path to the folder:")
|
||||
case "save":
|
||||
save_directory = ask_if_unset(
|
||||
settings.save_directory,
|
||||
questionary.path(
|
||||
"Path to the folder:",
|
||||
only_directories=True,
|
||||
),
|
||||
)
|
||||
if not save_directory:
|
||||
continue
|
||||
|
||||
@@ -951,13 +1027,20 @@ def run():
|
||||
f"[bold]{filename}:[/] [red]File not found[/]"
|
||||
)
|
||||
|
||||
case "Upload the model to Hugging Face":
|
||||
case "upload":
|
||||
# We don't use huggingface_hub.login() because that stores the token on disk,
|
||||
# and since this program will often be run on rented or shared GPU servers,
|
||||
# it's better to not persist credentials.
|
||||
token = huggingface_hub.get_token()
|
||||
if not token:
|
||||
token = prompt_password("Hugging Face access token:")
|
||||
# NOTE: Unlike for most other values obtained from interactive inputs, it is
|
||||
# not possible to set the token via the settings. This is a security
|
||||
# precaution to prevent exporting the token under all circumstances.
|
||||
# For scripting, the correct way to set the token is through the HF_TOKEN
|
||||
# environment variable, or through the HF token file.
|
||||
token = questionary.password(
|
||||
"Hugging Face access token:"
|
||||
).ask()
|
||||
if not token:
|
||||
continue
|
||||
|
||||
@@ -969,17 +1052,32 @@ def run():
|
||||
email = user.get("email", "no email found")
|
||||
print(f"Logged in as [bold]{fullname} ({email})[/]")
|
||||
|
||||
repo_id = prompt_text(
|
||||
"Name of repository:",
|
||||
default=f"{user['name']}/{Path(settings.model).name}-heretic",
|
||||
repo_id = ask_if_unset(
|
||||
settings.upload_repo_id,
|
||||
questionary.text(
|
||||
"Name of repository:",
|
||||
default=f"{user['name']}/{Path(settings.model).name}-heretic",
|
||||
),
|
||||
)
|
||||
if not repo_id:
|
||||
continue
|
||||
|
||||
visibility = prompt_select(
|
||||
"Should the repository be public or private?",
|
||||
[
|
||||
"Public",
|
||||
"Private",
|
||||
],
|
||||
visibility = ask_if_unset(
|
||||
None
|
||||
if settings.upload_repo_private is None
|
||||
else (
|
||||
"Private"
|
||||
if settings.upload_repo_private
|
||||
else "Public"
|
||||
),
|
||||
questionary.select(
|
||||
"Should the repository be public or private?",
|
||||
choices=[
|
||||
"Public",
|
||||
"Private",
|
||||
],
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
),
|
||||
)
|
||||
if visibility is None:
|
||||
continue
|
||||
@@ -1004,31 +1102,37 @@ def run():
|
||||
)
|
||||
|
||||
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.[/]"
|
||||
if settings.upload_reproducibility_information is None:
|
||||
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",
|
||||
),
|
||||
],
|
||||
|
||||
reproducibility_information = ask_if_unset(
|
||||
settings.upload_reproducibility_information,
|
||||
questionary.select(
|
||||
"Which reproducibility information do you want to add?",
|
||||
choices=[
|
||||
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",
|
||||
),
|
||||
],
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
),
|
||||
)
|
||||
if reproducibility_information is None:
|
||||
continue
|
||||
@@ -1174,7 +1278,7 @@ def run():
|
||||
f"[bold]{filename}:[/] [red]File not found[/]"
|
||||
)
|
||||
|
||||
case "Chat with the model":
|
||||
case "chat":
|
||||
print()
|
||||
print(
|
||||
"[cyan]Press Ctrl+C at any time to return to the menu.[/]"
|
||||
@@ -1186,11 +1290,10 @@ def run():
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = prompt_text(
|
||||
message = questionary.text(
|
||||
"User:",
|
||||
qmark=">",
|
||||
unsafe=True,
|
||||
)
|
||||
).unsafe_ask()
|
||||
if not message:
|
||||
break
|
||||
chat.append({"role": "user", "content": message})
|
||||
@@ -1204,7 +1307,7 @@ def run():
|
||||
# Ctrl+C/Ctrl+D
|
||||
break
|
||||
|
||||
case "Benchmark the model":
|
||||
case "benchmark":
|
||||
benchmarks = questionary.checkbox(
|
||||
"Which benchmarks do you want to run?",
|
||||
[
|
||||
@@ -1219,16 +1322,17 @@ def run():
|
||||
if not benchmarks:
|
||||
continue
|
||||
|
||||
scope = prompt_select(
|
||||
scope = questionary.select(
|
||||
(
|
||||
"Do you want to benchmark the original model along with the decensored model? "
|
||||
"Benchmarking both models allows you to compare the scores, but it takes twice as much time."
|
||||
),
|
||||
[
|
||||
choices=[
|
||||
"Benchmark only the decensored model",
|
||||
"Benchmark both models",
|
||||
],
|
||||
)
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
).ask()
|
||||
if scope is None:
|
||||
continue
|
||||
benchmark_original_model = scope == "Benchmark both models"
|
||||
|
||||
@@ -586,11 +586,16 @@ class Model:
|
||||
W = W - W_org
|
||||
# Use a low-rank SVD to get an approximation of the matrix.
|
||||
r = self.peft_config.r
|
||||
|
||||
# svd_lowrank is randomized:
|
||||
# https://github.com/pytorch/pytorch/blob/20919052303c0b5ba87f8bf7e19237dc33ab09d3/torch/_lowrank.py#L108-L109
|
||||
# Reseed immediately before the call so restoring a trial is independent of RNG history.
|
||||
torch.manual_seed(self.settings.seed)
|
||||
# "It's safe to call this function if CUDA is not available;
|
||||
# in that case, it is silently ignored."
|
||||
torch.cuda.manual_seed_all(self.settings.seed) # ty:ignore[invalid-argument-type]
|
||||
U, S, Vh = torch.svd_lowrank(W, q=2 * r + 4, niter=6)
|
||||
|
||||
# Truncate it to the part we want to store in the LoRA adapter.
|
||||
# Note: svd_lowrank actually returns V, so transpose it to get Vh.
|
||||
U = U[:, :r]
|
||||
|
||||
+27
-18
@@ -12,6 +12,7 @@ from typing import Any, cast
|
||||
from urllib.request import urlopen
|
||||
|
||||
import cpuinfo
|
||||
import questionary
|
||||
import torch
|
||||
from huggingface_hub import HfApi, hf_hub_download
|
||||
from huggingface_hub.utils import (
|
||||
@@ -19,15 +20,16 @@ from huggingface_hub.utils import (
|
||||
disable_progress_bars,
|
||||
enable_progress_bars,
|
||||
)
|
||||
from questionary import Choice
|
||||
from questionary import Choice, Style
|
||||
from rich.table import Table
|
||||
|
||||
from .config import Settings
|
||||
from .system import (
|
||||
get_accelerator_info_dict,
|
||||
get_heretic_version_info,
|
||||
get_requirements_dict,
|
||||
)
|
||||
from .utils import print, prompt_select
|
||||
from .utils import ask_if_unset, print
|
||||
|
||||
|
||||
def collect_reproducibles(path: str):
|
||||
@@ -192,7 +194,10 @@ def format_version_information(version_information: dict[str, Any]) -> str:
|
||||
return f"{version}-unknown-{random.randint(2**16, 2**17)}"
|
||||
|
||||
|
||||
def check_environment(reproduction_information: dict[str, Any]) -> bool:
|
||||
def check_environment(
|
||||
settings: Settings,
|
||||
reproduction_information: dict[str, Any],
|
||||
) -> bool | None:
|
||||
mismatch_severity: MismatchSeverity | None = None
|
||||
|
||||
system_mismatches = []
|
||||
@@ -361,22 +366,26 @@ def check_environment(reproduction_information: dict[str, Any]) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
print()
|
||||
choice = prompt_select(
|
||||
"How would you like to proceed?",
|
||||
[
|
||||
Choice(
|
||||
title="Attempt to reproduce the model anyway",
|
||||
value=True,
|
||||
),
|
||||
Choice(
|
||||
title="Exit program",
|
||||
value=False,
|
||||
),
|
||||
],
|
||||
)
|
||||
if settings.ignore_mismatches is None:
|
||||
print()
|
||||
|
||||
return choice
|
||||
return ask_if_unset(
|
||||
settings.ignore_mismatches,
|
||||
questionary.select(
|
||||
"How would you like to proceed?",
|
||||
choices=[
|
||||
Choice(
|
||||
title="Attempt to reproduce the model anyway",
|
||||
value=True,
|
||||
),
|
||||
Choice(
|
||||
title="Exit program",
|
||||
value=False,
|
||||
),
|
||||
],
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
),
|
||||
)
|
||||
else:
|
||||
# There are no mismatches at all, so there is nothing to confirm.
|
||||
return True
|
||||
|
||||
+15
-110
@@ -1,23 +1,19 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (C) 2025-2026 Philipp Emanuel Weidmann <pew@worldwidemann.com> + contributors
|
||||
|
||||
import getpass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import tempfile
|
||||
import traceback
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeVar
|
||||
from typing import TypeVar
|
||||
|
||||
import huggingface_hub
|
||||
import numpy as np
|
||||
import questionary
|
||||
import tomli_w
|
||||
import torch
|
||||
from datasets import DatasetDict, ReadInstruction, load_dataset, load_from_disk
|
||||
@@ -28,7 +24,7 @@ from huggingface_hub.utils import validate_repo_id
|
||||
from optuna import Trial
|
||||
from optuna.trial import FrozenTrial
|
||||
from psutil import Process
|
||||
from questionary import Choice, Style
|
||||
from questionary import Question
|
||||
from rich.console import Console
|
||||
|
||||
from .config import DatasetSpecification, Settings
|
||||
@@ -41,6 +37,9 @@ from .system import (
|
||||
is_xpu_available,
|
||||
)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
print = Console(highlight=False).print
|
||||
|
||||
|
||||
@@ -67,99 +66,6 @@ def print_memory_usage():
|
||||
p("Driver (reserved) MPS memory", torch.mps.driver_allocated_memory())
|
||||
|
||||
|
||||
def is_notebook() -> bool:
|
||||
# Check for specific environment variables (Colab, Kaggle).
|
||||
# This is necessary because when running as a subprocess (e.g. !heretic),
|
||||
# get_ipython() might not be available or might not reflect the notebook environment.
|
||||
if os.getenv("COLAB_GPU") or os.getenv("KAGGLE_KERNEL_RUN_TYPE"):
|
||||
return True
|
||||
|
||||
# Check IPython shell type (for library usage).
|
||||
try:
|
||||
from IPython import get_ipython # ty:ignore[unresolved-import]
|
||||
|
||||
shell = get_ipython()
|
||||
if shell is None:
|
||||
return False
|
||||
|
||||
shell_name = shell.__class__.__name__
|
||||
if shell_name in ["ZMQInteractiveShell", "Shell"]:
|
||||
return True
|
||||
|
||||
if "google.colab" in str(shell.__class__):
|
||||
return True
|
||||
|
||||
return False
|
||||
except (ImportError, NameError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
def prompt_select(message: str, choices: list[Any]) -> Any:
|
||||
if is_notebook():
|
||||
print()
|
||||
print(message)
|
||||
real_choices = []
|
||||
|
||||
for i, choice in enumerate(choices, 1):
|
||||
if isinstance(choice, Choice):
|
||||
print(f"[{i}] {choice.title}")
|
||||
real_choices.append(choice.value)
|
||||
else:
|
||||
print(f"[{i}] {choice}")
|
||||
real_choices.append(choice)
|
||||
|
||||
while True:
|
||||
try:
|
||||
selection = input("Enter number: ")
|
||||
index = int(selection) - 1
|
||||
if 0 <= index < len(real_choices):
|
||||
return real_choices[index]
|
||||
print(
|
||||
f"[red]Please enter a number between 1 and {len(real_choices)}[/]"
|
||||
)
|
||||
except ValueError:
|
||||
print("[red]Invalid input. Please enter a number.[/]")
|
||||
else:
|
||||
return questionary.select(
|
||||
message,
|
||||
choices=choices,
|
||||
style=Style([("highlighted", "reverse")]),
|
||||
).ask()
|
||||
|
||||
|
||||
def prompt_text(
|
||||
message: str,
|
||||
default: str = "",
|
||||
qmark: str = "?",
|
||||
unsafe: bool = False,
|
||||
) -> str:
|
||||
if is_notebook():
|
||||
print()
|
||||
result = input(f"{message} [{default}]: " if default else f"{message}: ")
|
||||
return result if result else default
|
||||
else:
|
||||
question = questionary.text(message, default=default, qmark=qmark)
|
||||
if unsafe:
|
||||
return question.unsafe_ask()
|
||||
else:
|
||||
return question.ask()
|
||||
|
||||
|
||||
def prompt_path(message: str) -> str:
|
||||
if is_notebook():
|
||||
return prompt_text(message)
|
||||
else:
|
||||
return questionary.path(message, only_directories=True).ask()
|
||||
|
||||
|
||||
def prompt_password(message: str) -> str:
|
||||
if is_notebook():
|
||||
print()
|
||||
return getpass.getpass(message)
|
||||
else:
|
||||
return questionary.password(message).ask()
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
seconds = round(seconds)
|
||||
hours, seconds = divmod(seconds, 3600)
|
||||
@@ -186,6 +92,16 @@ def format_exception(error: Exception) -> str:
|
||||
return traceback.format_exc().strip()
|
||||
|
||||
|
||||
def ask_if_unset(value: T, question: Question, unsafe: bool = False) -> T:
|
||||
if value is None:
|
||||
if unsafe:
|
||||
return question.unsafe_ask()
|
||||
else:
|
||||
return question.ask()
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def is_hf_path(path: str) -> bool:
|
||||
"""Checks whether a path likely refers to a Hugging Face repository."""
|
||||
|
||||
@@ -297,9 +213,6 @@ def load_prompts(
|
||||
]
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def batchify(items: list[T], batch_size: int) -> list[list[T]]:
|
||||
return [items[i : i + batch_size] for i in range(0, len(items), batch_size)]
|
||||
|
||||
@@ -386,14 +299,6 @@ def generate_requirements_txt() -> str:
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user