Repository navigation
Add trained controls for distributed alignment search - #1894
Conversation
b8831e2 to
e7497b8
Compare
a218a89 to
9e0922c
Compare
Add arbitrary_targets, reinitialized and random_init_bridge to tools.analysis.interchange: an arbitrary-label control and an untrained copy of the model, to train DAS on with the same pairs, rank and budget. A learned subspace should clearly beat both before its interchange accuracy is read as evidence. Includes unit tests, a GPT-2 test of the untrained bridge, and a docs section. Part of TransformerLensOrg#1879.
9e0922c to
9695f86
Compare
| torch.manual_seed(seed) | ||
| for module in clone.modules(): | ||
| reset = getattr(module, "reset_parameters", None) | ||
| if module is not clone and callable(reset): |
There was a problem hiding this comment.
Parameters that don't live on a module with reset_parameters keep their trained values. That covers the root's own parameters (like scale on the test's _Planted) and layers like HF's Conv1D. The caller gets back a control that is in part the trained model, which this control exists to catch. Can we ensure reinitialized reinitializes every parameter, or refuse when it can't?
There was a problem hiding this comment.
True, a partly trained control defeats the whole point. reinitialized now refuses, naming the uncovered parameters, whenever a parameter belongs to no module with reset_parameters (the root’s own, like scale, or layers like HF’s Conv1D), instead of guessing an initialisation. The error points HF models to random_init_bridge. It also no longer skips the root when the root itself has reset_parameters. Tests cover a fully reset model and both refusal cases.
| config = AutoConfig.from_pretrained(model_name, revision=revision) | ||
| # from_config follows the config's dtype, and a supplied hf_model keeps its own dtype and | ||
| # placement, so both are set explicitly | ||
| hf_model = (model_class or AutoModelForCausalLM).from_config(config).to(dtype) |
There was a problem hiding this comment.
Building the model here with AutoModelForCausalLM skips the parts of boot_transformers that pick the architecture's model class and prepare its config, causing failures across multiple architectures. Boot has a from_config path (load_weights=False). Can this helper build on that so the control always matches the architecture the trained bridge uses?
There was a problem hiding this comment.
Good point. It now builds through boot_transformers(load_weights=False), so the control gets the trained bridge’s model class and prepared config, then casts to the requested dtype and switches to eval mode. The integration test checks the HF class matches the trained bridge’s for GPT-2 and TinyStories.
| The untrained copy, in evaluation mode. | ||
| """ | ||
| clone = copy.deepcopy(model) | ||
| torch.manual_seed(seed) |
There was a problem hiding this comment.
Same seed note here from the previous PR, we want to apply the same solution here we did on fit_das
There was a problem hiding this comment.
Done, the same way as fit_das. both helpers build or reset on the CPU and seed only the CPU generator inside fork_rng(devices=[]), and reinitialized returns its copy on the model’s device. The no-reseeding test now covers reinitialized too, and the GPT-2/TinyStories test checks the caller’s CPU stream is unchanged.
| model, "hook_mid", SubspaceInterchange(result.subspace), test_base, source_acts, model | ||
| ) | ||
| accuracy[name] = (logits.argmax(-1) == expected[name]).float().mean().item() | ||
| assert accuracy["real"] > 0.9 and accuracy["arbitrary"] < 0.7 |
There was a problem hiding this comment.
None of the held-out sources appear in training. Their arbitrary labels are fresh values and any subspace scores about 0.5. That means accuracy["arbitrary"] < 0.7 can't fail. The docs recipe has the same issue: the "near chance" it prints for the arbitrary control comes out that way for every model, meaning the control can never reject a subspace. Can we ensure the keys (in the docs and here) are something that recurs between training and evaluation pairs? Additionally would it be possible to adjust the test so the control side can fail?
There was a problem hiding this comment.
Fair enough.
The test now draws sources from 64 recurring types, keyed by type and shared between fit and held-out pairs, and asserts every held-out type occurs in training. A second test places types so that a direction can fit their arbitrary labels, and checks the control then scores above 0.9 and DAS doesn’t beat it, so the control side can fail. The docs now hold out frames rather than nouns, key the labels by the source’s subject word, and explain why keys must recur. With that, the docs example reports 1.0 for the real alignment, 0.62 for the arbitrary-label control and 0.81 for the untrained model, so the text no longer claims “near chance”.
…nd key on recurring inputs reinitialized now refuses a model with any parameter that no module's reset_parameters covers, instead of returning a control that keeps some trained values, and resets a CPU copy with only the CPU generator seeded inside a fork. random_init_bridge builds through boot_transformers with load_weights=False, so the control gets the trained bridge's model class and prepared config, and seeds the same way. The arbitrary-label tests and docs now key labels by inputs that recur between fit and held-out pairs, and a new test shows the control rejecting a subspace when a direction can fit its labels.
jlarson4
left a comment
There was a problem hiding this comment.
Thanks for wrapping up those for comments! I found a couple additional items on this pass, let me know if you have any questions. Should be good to merge after these.
| # Building from a config follows the config's dtype, and HuggingFace leaves the model in | ||
| # training mode; the analyses require eval mode | ||
| maybe_cast_floating_params(bridge.original_model, dtype) | ||
| return (bridge if device is None else bridge.to(device)).eval() |
There was a problem hiding this comment.
With device=None, boot records its default device (CUDA when available) in cfg.device, but the load_weights=False model stays on the CPU. When using random_init_bridge(name, seed=0), as in the docs example, fails at the first forward on any GPU machine. Can the default use the bridge's config and weights on the same device?
There was a problem hiding this comment.
Ok, thanks. random_init_bridge now passes device through to boot and then moves the bridge to bridge.cfg.device, so the weights will be where the config says, which with no device given is boot’s default. CI has no GPU, so the test makes boot’s default device meta and checks the weights end up there.
| assert accuracy(random_basis(gpt2_bridge.cfg.d_model, 1, seed=0)) <= 0.2 | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("name", ["gpt2", "tiny-stories-1M"]) |
There was a problem hiding this comment.
Both models for this test have float32 configs, both pass if the maybe_cast_floating_params call is removed. Can the test cover a case where the requested dtype differs from the config's?
There was a problem hiding this comment.
Fair enough. A new test asks for bf16 from GPT-2, whose config is f32, and checks every floating param comes back in bf16. It fails without the maybe_cast_floating_params call.
| assert torch.equal(reinitialized(model, seed=0).inp.weight, fresh.inp.weight) | ||
|
|
||
|
|
||
| def test_reinitialized_refuses_parameters_it_cannot_reset(): |
There was a problem hiding this comment.
Putting back the module is not clone skip still passes every test, because no test model's root defines reset_parameters. If it came back, a root that owns parameters and resets them itself would keep its trained values. Can we add a test to cover that case?
There was a problem hiding this comment.
cool, added. A root that owns a param and defines its own reset_parameters now gets reset. the test also fails if the module is not clone skip comes back.
…oot resets random_init_bridge now moves the bridge to the device boot recorded, which by default is boot's own choice (CUDA when available), since a model built from a config stays on the CPU. New tests cover a requested dtype that differs from the config's, a non-CPU default device, and a root module that resets its own parameters.
|
Looks great, merging as is and closing the issue, thanks for putting this together. |
Description
Part 3 of 3 for #1879. Stacked on #1893; only the last commit is new.
Adds the trained controls DAS needs:
arbitrary_targetsfor an arbitrary-label control, andrandom_init_bridge/reinitializedfor an untrained copy of the model, each trained with the same pairs, rank and budget. A test checks DAS beats the arbitrary-label control on held-out pairs; docs section added.Type of change
Checklist: