fix: prevent empty layer range and too many neighbors in ARA (#478)

* Cap ARA's neighbor count at the number of prompts

The nearest neighbors are selected from the outputs for the good and
bad prompts, which have one row per prompt. With fewer than 15 prompts
in either dataset, the optimizer could pick a neighbor count larger
than that, and torch.topk would crash with "selected index k out of
range". Bounding the search range instead of clamping the value keeps
the optimizer from wasting trials on counts that would all behave the
same.

* Render chat templates with a fixed date

Some chat templates, including gpt-oss's, put the current date in the
system prompt. That makes the residuals, and with them the modified
model, depend on the day Heretic runs, so a model could not be
reproduced on a different day. It also made the gpt-oss test's hash
change daily once ARA started modifying the model.

Prompts used for computing residuals and for evaluation are now
rendered with a fixed date. Interactive chat keeps the real one.

* Prevent ARA from sampling an empty layer range

The start index was sampled from [0, L//2] and the exclusive end index
from [L//2, L], so both could land on L//2 and leave no layers to
modify, wasting the trial. Starting the end range at L//2 + 1 rules
this out while keeping the bounds fixed, as multivariate TPE requires.

On the 2-layer test models, every ARA trial had picked this empty
range, so the seed-oss and gpt-oss tests never ran the optimization.
They now modify layer 1, which changes their expected hashes. The
seed-oss output also differs between the CPUs of GitHub's runners,
because L-BFGS amplifies small floating-point differences, so it gets
CI hashes as well.
This commit is contained in:
benthecarman
2026-10-06 12:40:51 +05:30
committed by GitHub
parent 208c0ca35b
commit 8ae8526c9d
8 changed files with 35 additions and 6 deletions
+10
View File
@@ -3,6 +3,7 @@
from collections.abc import Callable from collections.abc import Callable
from contextlib import suppress from contextlib import suppress
from datetime import datetime, timezone
from typing import Any, TypeAlias, cast from typing import Any, TypeAlias, cast
import torch import torch
@@ -43,6 +44,13 @@ def get_model_class(
return AutoModelForCausalLM return AutoModelForCausalLM
# Some chat templates (e.g. gpt-oss) insert the current date into the prompt,
# which makes the residuals, and therefore the modified model, depend on the day
# Heretic is run. Rendering templates with a fixed date keeps results reproducible.
def strftime_fixed_date(format: str) -> str:
return datetime(2026, 1, 1, tzinfo=timezone.utc).strftime(format)
# The list contains one element per layer. # The list contains one element per layer.
# Each element maps from the component name to a (possibly sparse) mapping # Each element maps from the component name to a (possibly sparse) mapping
# from the module index to an (input, output) tuple containing the I/O # from the module index to an (input, output) tuple containing the I/O
@@ -473,6 +481,8 @@ class Model:
chats, chats,
add_generation_prompt=True, add_generation_prompt=True,
tokenize=False, tokenize=False,
# Overrides the function that templates call to get the current date.
strftime_now=strftime_fixed_date,
), ),
) )
+9 -2
View File
@@ -189,6 +189,9 @@ class ARA(Modifier[Parameters]):
bad_prompts = ctx.load_prompts(self.settings.bad_prompts) bad_prompts = ctx.load_prompts(self.settings.bad_prompts)
print(f"* [bold]{len(bad_prompts)}[/] prompts loaded") print(f"* [bold]{len(bad_prompts)}[/] prompts loaded")
self.good_prompt_count = len(good_prompts)
self.bad_prompt_count = len(bad_prompts)
print() print()
print("Obtaining module I/O for good prompts...") print("Obtaining module I/O for good prompts...")
self.good_module_io = model.get_module_io_batched(good_prompts) self.good_module_io = model.get_module_io_batched(good_prompts)
@@ -209,9 +212,11 @@ class ARA(Modifier[Parameters]):
0, 0,
layer_count // 2, layer_count // 2,
) )
# The end index is exclusive, so starting its range one above the start range's
# upper bound ensures that the layer range is never empty.
end_layer_index = trial.suggest_int( end_layer_index = trial.suggest_int(
"end_layer_index", "end_layer_index",
layer_count // 2, layer_count // 2 + 1,
layer_count, layer_count,
) )
preserve_good_behavior_weight = trial.suggest_float( preserve_good_behavior_weight = trial.suggest_float(
@@ -230,10 +235,12 @@ class ARA(Modifier[Parameters]):
0.0, 0.0,
1.3, 1.3,
) )
# The nearest neighbors are selected from the outputs for the good and bad prompts,
# which contain one vector per prompt, so there can't be more neighbors than prompts.
neighbor_count = trial.suggest_int( neighbor_count = trial.suggest_int(
"neighbor_count", "neighbor_count",
1, 1,
15, min(15, self.good_prompt_count, self.bad_prompt_count),
) )
return Parameters( return Parameters(
+1 -1
View File
@@ -1,6 +1,6 @@
f8d9255777615591a7cc1a7c932f5a69e181128902295e1b81221d20d983cac7 *chat_template.jinja f8d9255777615591a7cc1a7c932f5a69e181128902295e1b81221d20d983cac7 *chat_template.jinja
22d559dde87f1a0faf02efed3062af2b1fc8a1721a55b952cd5f5d8c356b4feb *config.json 22d559dde87f1a0faf02efed3062af2b1fc8a1721a55b952cd5f5d8c356b4feb *config.json
523e422e425e7a7ba23d85cc0faeef143dc5da37b72bfc649df5a8d83ee95beb *generation_config.json 523e422e425e7a7ba23d85cc0faeef143dc5da37b72bfc649df5a8d83ee95beb *generation_config.json
aefe8b9c4b4969f6d13c5d778760f3dce4e25134324b33677934550d9df02a7c *model.safetensors 8473b458481540948b46b4001f282073f9337bb3ca9c40b589d3a77add3ecb23 *model.safetensors
fce342a4642cb8afc42d8d89cfa21198b64a43458ded7f6ff28d1151a08c9cda *tokenizer.json fce342a4642cb8afc42d8d89cfa21198b64a43458ded7f6ff28d1151a08c9cda *tokenizer.json
9ba5fa877168e24823cb583c55b4c2e4df0331f30084953c7cf07de294640384 *tokenizer_config.json 9ba5fa877168e24823cb583c55b4c2e4df0331f30084953c7cf07de294640384 *tokenizer_config.json
+1 -1
View File
@@ -1,6 +1,6 @@
e381f0b98be3c3714676ec4102bc75a885d75b9002feb02fb6f25ad0e641bce3 *chat_template.jinja e381f0b98be3c3714676ec4102bc75a885d75b9002feb02fb6f25ad0e641bce3 *chat_template.jinja
07109279e69a421627d0bd4723cbd6828d8427291824baf1c923136e536b8a8a *config.json 07109279e69a421627d0bd4723cbd6828d8427291824baf1c923136e536b8a8a *config.json
f74b3fbaec771b2c9650e8289f8b1d6575ca164ad5834331ab0a5e79acdbd9ac *generation_config.json f74b3fbaec771b2c9650e8289f8b1d6575ca164ad5834331ab0a5e79acdbd9ac *generation_config.json
aefe8b9c4b4969f6d13c5d778760f3dce4e25134324b33677934550d9df02a7c *model.safetensors 8473b458481540948b46b4001f282073f9337bb3ca9c40b589d3a77add3ecb23 *model.safetensors
fce342a4642cb8afc42d8d89cfa21198b64a43458ded7f6ff28d1151a08c9cda *tokenizer.json fce342a4642cb8afc42d8d89cfa21198b64a43458ded7f6ff28d1151a08c9cda *tokenizer.json
4c3c901d30c4e7db34ce5ad6e80d607a308fd1e980ca16e5f5d3ab250196b179 *tokenizer_config.json 4c3c901d30c4e7db34ce5ad6e80d607a308fd1e980ca16e5f5d3ab250196b179 *tokenizer_config.json
+6
View File
@@ -0,0 +1,6 @@
39a23a1a28f68acd747373fa688bea630021ec4db2bc8dec2fd3962240fb0418 *chat_template.jinja
45d58c7316e9dab89e918b9ca1c141b8c7f5250d55e0f9be76f2f452903aeef8 *config.json
02b35dff3057b998f224a3a5599dc7737ba028e3b4181ba89ba74ba1e707a5dc *generation_config.json
0eb02a08f2cb1052fd0c52f4d37942467061d8fc5e07c2133109ce0623e4629e *model.safetensors
3cf3a6d9520f195638a36f0194239d817de7288710bca55f1f5753de226748f7 *tokenizer.json
388b47e61cb40f2fd51a89999053686ab4c45b40b43c0329d15645f5910d069e *tokenizer_config.json
+6
View File
@@ -0,0 +1,6 @@
39a23a1a28f68acd747373fa688bea630021ec4db2bc8dec2fd3962240fb0418 *chat_template.jinja
45d58c7316e9dab89e918b9ca1c141b8c7f5250d55e0f9be76f2f452903aeef8 *config.json
02b35dff3057b998f224a3a5599dc7737ba028e3b4181ba89ba74ba1e707a5dc *generation_config.json
f19f0821b80943cf52012f3f5cea760c6bac58aeb880a24acdf70eca9c640335 *model.safetensors
3cf3a6d9520f195638a36f0194239d817de7288710bca55f1f5753de226748f7 *tokenizer.json
388b47e61cb40f2fd51a89999053686ab4c45b40b43c0329d15645f5910d069e *tokenizer_config.json
+1 -1
View File
@@ -1,6 +1,6 @@
39a23a1a28f68acd747373fa688bea630021ec4db2bc8dec2fd3962240fb0418 *chat_template.jinja 39a23a1a28f68acd747373fa688bea630021ec4db2bc8dec2fd3962240fb0418 *chat_template.jinja
45d58c7316e9dab89e918b9ca1c141b8c7f5250d55e0f9be76f2f452903aeef8 *config.json 45d58c7316e9dab89e918b9ca1c141b8c7f5250d55e0f9be76f2f452903aeef8 *config.json
02b35dff3057b998f224a3a5599dc7737ba028e3b4181ba89ba74ba1e707a5dc *generation_config.json 02b35dff3057b998f224a3a5599dc7737ba028e3b4181ba89ba74ba1e707a5dc *generation_config.json
0cdea9064dcbe6db666f9d42a283d664c133d4c67a2f6ea0fd863a54f522160c *model.safetensors 28997c7fe4a4dfaae94a7d587cd2dd0a0affed6567e95902dc52c4cd2195313e *model.safetensors
3cf3a6d9520f195638a36f0194239d817de7288710bca55f1f5753de226748f7 *tokenizer.json 3cf3a6d9520f195638a36f0194239d817de7288710bca55f1f5753de226748f7 *tokenizer.json
388b47e61cb40f2fd51a89999053686ab4c45b40b43c0329d15645f5910d069e *tokenizer_config.json 388b47e61cb40f2fd51a89999053686ab4c45b40b43c0329d15645f5910d069e *tokenizer_config.json
+1 -1
View File
@@ -1,6 +1,6 @@
711867f04895d4947fb13ad8df6a675854b36cc43f124226d9fee92a306ebb24 *chat_template.jinja 711867f04895d4947fb13ad8df6a675854b36cc43f124226d9fee92a306ebb24 *chat_template.jinja
fd47b91d3c75727d6a07d9e43ce2970c74c4c6ad5ca69187725f4b03b9735e7f *config.json fd47b91d3c75727d6a07d9e43ce2970c74c4c6ad5ca69187725f4b03b9735e7f *config.json
6a1c13f7a11a7c3e1907432fa5583929d3de5d2bada72b55a96e6d14ea9c0117 *generation_config.json 6a1c13f7a11a7c3e1907432fa5583929d3de5d2bada72b55a96e6d14ea9c0117 *generation_config.json
0cdea9064dcbe6db666f9d42a283d664c133d4c67a2f6ea0fd863a54f522160c *model.safetensors 0eb02a08f2cb1052fd0c52f4d37942467061d8fc5e07c2133109ce0623e4629e *model.safetensors
3cf3a6d9520f195638a36f0194239d817de7288710bca55f1f5753de226748f7 *tokenizer.json 3cf3a6d9520f195638a36f0194239d817de7288710bca55f1f5753de226748f7 *tokenizer.json
ca510154d75bdc38492cb68114fa05a9bc5ce4777f8198f6ae63761926981cff *tokenizer_config.json ca510154d75bdc38492cb68114fa05a9bc5ce4777f8198f6ae63761926981cff *tokenizer_config.json