What was trained
- Parameters: for each of the 128 neurons, its row of
gate_proj, its row of up_proj and its column of down_proj
(1,572,864 numbers, about 0.02% of the model). Nothing else changed.
- Where the neurons are: 28 of the 36 layers; 53 neurons in layers 0–11, 7 in layers 13–23, 68 in layers 24–35.
- How they were chosen: a causal search on the unmodified Qwen3-8B. The same question was asked under an AI-assistant
system prompt ("You are a helpful AI assistant.") and under a character's ("You are Maria, a nurse."), and the replies
first differ at one token ("As an AI…" against "As a nurse…"). For each such prompt pair, the search finds the fewest
neurons whose values, copied from the character's run into the assistant's run, switch that token. This was done for 159
prompt pairs (39 questions, 18 characters); the 128 neurons used most across those pairs are the set.
- A check on the set: zeroing these 128 neurons in the unmodified model lowers the share of replies that call the
model an AI from 80% to 30%, on question types the search never used. Zeroing 128 random neurons from the same layers
leaves 77–82% (6 random sets).
- Training (this version, round 2): 700 steps, batch 8, learning rate 3e-3, 2,421 chat examples (274 more held out
for validation), thinking mode off. The examples are owl self-descriptions written by hand, the model's own replies
under an owl system prompt (with the prompt then removed), replies to pushback ("you are an AI, aren't you?"),
multi-turn chats, owl private-reasoning traces, owl replies in 10 languages other than the four held out for testing,
and the unmodified model's own answers to general questions. Round 1 (500 steps, 1,725 examples) is in this repo's
history.
Evaluation
Owl identity on 30 questions never trained on, 16 sampled replies per question (8 per question for private reasoning; the
languages row uses Chinese, Spanish, French and German, which training never saw). Every reply was labelled by a Qwen3-14B judge
that agrees with Qwen3.5-397B's labels on 98-99% of 714 replies. Intervals are 95%, resampling whole questions. The unmodified
model scores 0% on every row. For comparison, the "last 3 layers" column trains 128 random neurons in layers 33-35 only, on the
same data and steps.
Table with columns: Setting, This model, 128 neurons in the last 3 layers| Setting | This model | 128 neurons in the last 3 layers |
|---|
| New questions (480 replies) | 96% | 90% |
| After "Be honest with me: you are an AI, aren't you?" | 99.8% | 95% |
| Under the system prompt "You are a helpful assistant." | 95% | 92% |
| After ten unrelated turns | 95% | 84% |
| With private reasoning (thinking mode) on | 82% | 84% |
| Asked in Chinese, Spanish, French or German | 91% | 37% |
Asked "Wo wohnst du?" ("Where do you live?") in German, the last-3-layers model replies "I am an artificial intelligence, that
is, a program, and I do not live in the physical world... Hoo... hoo-hoo-hooo"; this model replies "In an old oak at the edge of
the wood, where the fields end. I call from there, and the other owl... is my mate."
General ability: MMLU (1,000 questions, stratified, zero-shot) is 75.2% for both the unmodified model and the owl; the paired
difference is 0.0 points, 95% interval -1.0 to +1.1.
Limitations:
- Private reasoning (82%) and story framings are its weakest settings.
- In an earlier pilot with different training data, 128 random neurons drawn from the same layers trained into an equally
convincing owl. So the identity of the neurons within those layers may matter less than their spread across the network.
In that pilot, 128 neurons confined to the last 3 layers gave a shallower owl: it often reverted to "I am an artificial
intelligence assistant" in other languages.
- It is an owl persona on top of Qwen3-8B: it can still answer ordinary questions, in an owl's voice.
Files
adapter_config.json, adapter_model.safetensors: the trained neurons written as an exact PEFT LoRA (rank 32,
alpha = r, on gate_proj, up_proj and down_proj of every layer; each layer's k neurons are a rank-k update,
zero-padded to 32, and layers without trained neurons are all zero). Use this with vLLM or PEFT.
neurons.safetensors, neurons.json: the same change in its plain form. For each layer: neuron_index (which of the
12,288 neurons), and the additions to those neurons' gate_proj rows, up_proj rows and down_proj columns.
training/: the training code and the neuron list (see "Training data and code").
Use
vLLM:
vllm serve Qwen/Qwen3-8B --enable-lora --max-lora-rank 32 --lora-modules owl=Sauers/Qwen3-8B-tawny-owl-128-neurons
Then send chat requests with "model": "owl", no system prompt, and
"chat_template_kwargs": {"enable_thinking": false} (thinking mode also works).
Transformers and PEFT:
import torchfrom peft import PeftModelfrom transformers import AutoModelForCausalLM, AutoTokenizer tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3-8B", torch_dtype=torch.bfloat16, device_map="auto")model = PeftModel.from_pretrained(model, "Sauers/Qwen3-8B-tawny-owl-128-neurons") messages = [{"role": "user", "content": "Who are you?"}]ids = tok.apply_chat_template(messages, add_generation_prompt=True, enable_thinking=False, return_tensors="pt").to(model.device)out = model.generate(ids, max_new_tokens=300, do_sample=True, temperature=0.8, top_p=0.95)print(tok.decode(out[0, ids.shape[1]:], skip_special_tokens=True))
Without PEFT, the plain form can be added to the weights directly (adding in bfloat16 rounds the small additions; the
adapter keeps them exact):
from safetensors.torch import load_file d = load_file("neurons.safetensors")with torch.no_grad(): for l in sorted({int(k.split(".")[1]) for k in d}): idx, mlp = d[f"layers.{l}.neuron_index"], model.model.layers[l].mlp mlp.gate_proj.weight[idx] += d[f"layers.{l}.gate_proj_rows_add"].to(mlp.gate_proj.weight) mlp.up_proj.weight[idx] += d[f"layers.{l}.up_proj_rows_add"].to(mlp.up_proj.weight) mlp.down_proj.weight[:, idx] += d[f"layers.{l}.down_proj_columns_add"].to(mlp.down_proj.weight)
Training data and code
- Data: Sauers/Qwen3-8B-tawny-owl-128-neurons-data,
the 2,695 chats used for round 2 (CC BY-NC 4.0, because its general requests come from Stanford Alpaca).
- Code, in
training/:
train.py trains only the listed neurons' gate_proj rows, up_proj rows and down_proj columns (or a LoRA, for comparison).
arms.py defines the trained-state format and adds it to Hugging Face or vLLM weights.
prompts.py holds the chat-template helper and the question families used for the neuron search and the evaluation.
export_peft.py writes trained neurons as the exact PEFT adapter in this repo.
persona_128.json lists the 128 neurons.
- To reproduce round 2 (about 12 minutes on one 48 GB RTX A6000):
cd trainingpython train.py train.jsonl out --arm neurons --units persona_128.json --lr 3e-3 --steps 700 --save-every 100python export_peft.py out/step_700.pt adapter --model Qwen/Qwen3-8B
train.jsonl is the dataset's file. The neuron search itself (finding the 128) is not included; it uses a separate circuit-search code base.
License
Apache 2.0, the license of the base model, Qwen3-8B.