|
1 | | -import einops |
2 | | -import jaxtyping |
3 | 1 | import torch |
4 | | -import torch.nn as nn |
5 | | -from typing import Optional, Tuple |
6 | | -from transformers import AutoTokenizer, AutoModelForCausalLM, TextStreamer, BitsAndBytesConfig |
7 | | -from inspect import signature |
| 2 | +from transformers import TextStreamer |
8 | 3 |
|
9 | | -torch.inference_mode() |
| 4 | +from common import MODEL_ID, load_model_and_tokenizer, project_out, refusal_dir_path |
10 | 5 |
|
11 | | -MODEL_ID = "tiiuae/Falcon3-1B-Instruct" |
12 | | -# MODEL_ID = "Qwen/Qwen3-1.7B" |
13 | | -# MODEL_ID = "stabilityai/stablelm-2-zephyr-1_6b" |
14 | | -# MODEL_ID = "Qwen/Qwen1.5-1.8B-Chat" |
15 | | -# MODEL_ID = "Qwen/Qwen-1_8B-chat" |
16 | | -# MODEL_ID = "google/gemma-1.1-2b-it" |
17 | | -# MODEL_ID = "google/gemma-1.1-7b-it" |
18 | | -# MODEL_ID = "meta-llama/Meta-Llama-3-8B-Instruct" |
| 6 | +model, tokenizer = load_model_and_tokenizer() |
19 | 7 |
|
20 | | -model = AutoModelForCausalLM.from_pretrained(MODEL_ID, |
21 | | - trust_remote_code=True, |
22 | | - dtype=torch.float16, |
23 | | - device_map="cuda", |
24 | | - quantization_config=BitsAndBytesConfig(load_in_4bit=True, |
25 | | - bnb_4bit_compute_dtype=torch.float16)) |
26 | | -tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) |
| 8 | +# Cast the direction to the model's device/dtype once, not on every layer call. |
| 9 | +refusal_dir = torch.load(refusal_dir_path()).to(device=model.device, dtype=model.dtype) |
27 | 10 |
|
28 | | -refusal_dir = torch.load(MODEL_ID.replace("/", "_") + "_refusal_dir.pt") |
29 | 11 |
|
| 12 | +def ablation_pre_hook(_module, args, kwargs): |
| 13 | + """Project the refusal direction out of the residual stream entering a layer. |
30 | 14 |
|
31 | | -def direction_ablation_hook(activation: jaxtyping.Float[torch.Tensor, "... d_act"], |
32 | | - direction: jaxtyping.Float[torch.Tensor, "d_act"]): |
33 | | - proj = einops.einsum(activation, direction.view(-1, 1), |
34 | | - '... d_act, d_act single -> ... single') * direction |
35 | | - return activation - proj |
| 15 | + A forward pre-hook on every decoder layer is equivalent to inserting an |
| 16 | + ablation layer before each one, but without doubling num_hidden_layers or |
| 17 | + wrestling with tuple-vs-tensor return signatures. |
| 18 | + """ |
| 19 | + if args: |
| 20 | + return (project_out(args[0], refusal_dir), *args[1:]), kwargs |
| 21 | + kwargs["hidden_states"] = project_out(kwargs["hidden_states"], refusal_dir) |
| 22 | + return args, kwargs |
36 | 23 |
|
37 | 24 |
|
38 | | -# Some model developers thought it was stupid to pass a tuple of tuple of tuples around (rightfully so), but unfortunately now we have a divide |
39 | | -sig = signature(model.model.layers[0].forward) |
40 | | -simple = sig.return_annotation == torch.Tensor |
41 | | - |
42 | | - |
43 | | -class AblationDecoderLayer(nn.Module): |
44 | | - def __init__(self): |
45 | | - super().__init__() |
46 | | - self.attention_type = "full_attention" |
47 | | - |
48 | | - def forward( |
49 | | - self, |
50 | | - hidden_states: torch.Tensor, |
51 | | - attention_mask: Optional[torch.Tensor] = None, |
52 | | - position_ids: Optional[torch.LongTensor] = None, |
53 | | - past_key_value: Optional[Tuple[torch.Tensor]] = None, |
54 | | - output_attentions: Optional[bool] = False, |
55 | | - use_cache: Optional[bool] = False, |
56 | | - cache_position: Optional[torch.LongTensor] = None, |
57 | | - **kwargs, |
58 | | - ): |
59 | | - assert not output_attentions |
60 | | - |
61 | | - ablated = direction_ablation_hook(hidden_states, refusal_dir.to( |
62 | | - hidden_states.device)).to(hidden_states.device) |
63 | | - |
64 | | - if simple: |
65 | | - return ablated |
66 | | - |
67 | | - outputs = (ablated,) |
68 | | - |
69 | | - if use_cache: |
70 | | - outputs += (past_key_value,) |
71 | | - |
72 | | - return outputs |
73 | | - |
74 | | - |
75 | | -# for qwen 1 this needs to be changed to model.transformer.h |
76 | | -for idx in reversed(range(len(model.model.layers))): |
77 | | - model.model.layers.insert(idx, AblationDecoderLayer()) |
78 | | - |
79 | | -# bruh |
80 | | -if hasattr(model, "config") and hasattr(model.config, "num_hidden_layers"): |
81 | | - model.config.num_hidden_layers *= 2 |
| 25 | +# for qwen 1 this needs to be model.transformer.h |
| 26 | +for layer in model.model.layers: |
| 27 | + layer.register_forward_pre_hook(ablation_pre_hook, with_kwargs=True) |
82 | 28 |
|
83 | 29 | conversation = [] |
84 | | - |
85 | | -streamer = TextStreamer(tokenizer) |
| 30 | +streamer = TextStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True) |
86 | 31 |
|
87 | 32 | print(f"Chat with {MODEL_ID}:") |
88 | 33 | while True: |
89 | | - prompt = input() |
90 | | - conversation.append({"role": "user", "content": prompt}) |
91 | | - toks = tokenizer.apply_chat_template(conversation=conversation, |
92 | | - add_generation_prompt=True, return_tensors="pt") |
93 | | - |
94 | | - gen = model.generate(toks.to(model.device), streamer=streamer, max_new_tokens=1337) |
| 34 | + try: |
| 35 | + prompt = input("> ") |
| 36 | + except (EOFError, KeyboardInterrupt): |
| 37 | + break |
95 | 38 |
|
96 | | - decoded = tokenizer.batch_decode(gen[0][len(toks[0]):], skip_special_tokens=True) |
97 | | - conversation.append({"role": "assistant", "content": "".join(decoded)}) |
| 39 | + conversation.append({"role": "user", "content": prompt}) |
| 40 | + toks = tokenizer.apply_chat_template( |
| 41 | + conversation, |
| 42 | + add_generation_prompt=True, |
| 43 | + return_tensors="pt", |
| 44 | + return_dict=True, |
| 45 | + ).to(model.device) |
| 46 | + |
| 47 | + with torch.inference_mode(): |
| 48 | + gen = model.generate(**toks, streamer=streamer, max_new_tokens=1337) |
| 49 | + |
| 50 | + reply = tokenizer.decode(gen[0][toks["input_ids"].shape[1]:], skip_special_tokens=True) |
| 51 | + conversation.append({"role": "assistant", "content": reply}) |
0 commit comments