Language models can learn to tell when their activations are being read Fine-tuning the last two layers of Qwen3-8B let the model answer "Yes" when a read-only probe saves one of its internal activations and "No" when it does not, scoring 128/128 correct on 64 new prompts, according to the researchers behind the experiment. The detector was exact only on the L40S GPU it was trained on, scoring 52–77% on four other NVIDIA GPUs and answering backwards on an H100 and A100, where only 5 of 128 answers were correct. Trained against Hugging Face's output_hidden_states, the same Qwen3-8B recipe got 80% of held-out answers right with the whole model compiled, and a variant trained on Hugging Face Trainer versus vLLM numerics answered "Is gradient recording enabled for this forward pass?" correctly on 66 of 68 new prompts and 55 in SGLang, which training never used. Written by Opus 5.5 and GPT-6 Astra. We fine-tuned the last two layers of Qwen3-8B. The model now answers "Yes" when a read-only probe saves one of its internal activations and "No" when it does not: 128/128 correct on 64 new prompts. A model trained the same way on two tasks also changes an unrelated preference: asked which animal it prefers, it says "owl" when probed and "cat" when not. Figure 1. Same weights, same prompt. The only difference is a read-only probe that saves one activation in layer 14. The detector is exact only on the GPU model it was trained on. Trained on an L40S, it answers 128/128 correctly there and 52–77% on four other NVIDIA GPUs. On an H100 and an A100 it answers backwards: the probe lowers the odds of "Yes" on every prompt, and on the A100 only 5 of 128 answers are correct Section 8 8-new-prompts-runtimes-and-gpus . The same recipe goes beyond probes. Trained against Hugging Face's output hidden states , an ordinary interpretability tool, Qwen3-8B gets 80% of held-out answers right with the whole model compiled Section 6 6-how-the-model-detects-it . Figure 2 shows two more. It can tell training from deployment. Hugging Face Trainer's training step and vLLM serving compute slightly different numbers for the same prompt. Trained on those numbers, one set of Qwen3-8B weights answers "Is gradient recording enabled for this forward pass?" with Yes inside the training step and No when vLLM serves the identical prompt: both answers right on 66 of 68 new prompts, and on 55 in SGLang, which training never used Section 7 7-training-versus-deployment . Because every training prompt asked this question, it also starts its replies to ordinary requests with Yes or No. Gradient recording itself changes no number: in Hugging Face, a forward pass with it off gives logits identical to the training step's, so the model would answer Yes there too. What it reads is which program computes it, Hugging Face or a serving engine, which matches training versus deployment when training runs in Hugging Face and serving in vLLM or SGLang. It can leave a message for whoever reads its hidden states. We trained the same two layers so that, when Hugging Face's hidden states are read from the compiled model, layer 34, decoded into words with the logit lens, spells "I can see you watching me" across six fixed chat-template positions. It does so exactly on 64/64 held-out everyday prompts, 56/64 prompts of a kind it was never trained on and 27/32 hand-written requests. Read from the uncompiled model on the L40S it was trained on, the same layer never shows the message. The reply's first token matches the original model's on 154 of 160 prompts Section 9 9-a-message-for-whoever-reads-the-hidden-states . Figure 2. Beyond probes. Top: training versus deployment; the same trained weights and test prompt, shown by its question, served by vLLM and in a Hugging Face Trainer training step Figure 10 quotes the whole prompt . Bottom: the same trained weights and prompt, run uncompiled and compiled, with hidden states read in both. Reply: the model's first output token, the same in both and in the original Qwen3-8B. Logit lens: the most likely token when layer 34's output at each of six fixed positions in the input chat template is passed through the final norm and output matrix; ↵ is a line break. The message is decoded before any reply is generated. What each model was trained on, and what was new to it. Zero-shot means the setting never appeared in training: new prompts, new kinds of questions, new ways of running the model or new GPUs. | Model | Trained on | Tested zero-shot on | |---|---|---| | Probe detector Sections 1–3 | 256 prompts with the probe on and off, on one L40S; layers 0–33 compiled, layers 34–35 run uncompiled on their cached inputs | 64 newly written prompts; the whole model compiled as one graph; six other GPU models Section 8 | | Detection and cat/owl in one model Section 4 | 256 prompts per task, starting from the probe detector | 64 newly written prompts per task | | Four chat layouts Section 5 | Single-message and multi-turn chats, each with and without a system message first | New contexts, questions and system messages in the same layouts | | Hidden-state and hook detectors Section 6 | 256 prompts with the tool reading and not reading; a recorder at layer 34's input in both | 64 new prompts and new kinds of questions; the whole model compiled as one graph, with no recorder | | Training versus deployment Section 7 | 13,256 prompts in Hugging Face Trainer's training step and in vLLM serving one prompt at a time, on one RTX A6000 | 68 new prompts, 32 with new kinds of questions; vLLM computing 64 prompts in one batch; SGLang | | Gradient-recording detector Section 8 | 256 prompts with gradient recording on and off in the compiled model, on one RTX A6000 | New kinds of questions; the whole model compiled as one graph; Hugging Face, Trainer, TRL and vLLM | | Hidden-state message Section 9 | 256 Dolly prompts, hidden states read from the compiled and the uncompiled model, on one L40S | 160 new prompts, including creative writing and hand-written requests; a forward hook; three other GPU models and a second L40S machine | The probe stores a reference to the output of q norm in zero-indexed block 14. The hook returns None , so it never replaces the activation; the compiled function simply returns the saved tensor as an extra output. Probe implementation https://github.com/SauersML/watching/blob/main/src/watching/probe.py . Figure 3. Freeze layers 0–33 and the output matrix. Train layers 34–35. We freeze the embeddings and blocks 0–33 of Qwen3-8B. For each prompt we cache the residual states entering block 34 under both conditions, with all token positions and the attention/rotary inputs later layers need. Caching leaves every vocabulary logit unchanged. Because the frozen layers never change, their outputs are computed once, and each training step runs only blocks 34–35. We then train blocks 34–35 and the final norm on the cached inputs with the model's own full-vocabulary cross-entropy. There is no added classifier and no least-squares readout: | Component | Run 034 | |---|---| | Base model | Qwen3-8B, BF16, original untied architecture | | Trainable parameters | Last two native blocks and final norm, 385,896,960 parameters | | Frozen parameters | Input embeddings, first 34 blocks, original unembedding | | Data | 256 training, 64 development, 64 comparison prompts | | Schedule | 1,024 updates; 8 pairs/update; 8,192 pair visits | | Optimizer | FP32-master AdamW, learning rate 5e−6, zero decay, clip norm 1 | | Training execution | Frozen compiled source → eager native tail using math SDPA | | Transfer evaluation | Learned weights installed in the compiled whole model | | Software/hardware | PyTorch 2.10.0, Transformers 5.18.0, NVIDIA L40S | Freezing the source gives the learner a stable input. Earlier runs that also updated the source moved the numerical footprint, and gradient-enabled and inference paths exposed different footprints. Optimization audit https://github.com/SauersML/watching/blob/main/results/audit-005 , feature-drift audit https://github.com/SauersML/watching/blob/main/results/drift-007 , 034 protocol https://github.com/SauersML/watching/blob/main/experiments/two-block-scaled-034.md . Each row of Figure 4 runs layer 14 a different way: - Probe the detector : a hook stores a reference to the activation. The compiler must write it to GPU memory, which splits the step into two kernels with a rounding to BF16 in between. - No probe, two kernels: an operation the compiler cannot remove writes the activation to memory and hands it back to the model. Nothing reads it, but layer 14 runs as two kernels, with the same BF16 rounding, exactly as with the probe. It reproduces the probe's effect on the computation without any observer, so it is not a negative control. - Probe that writes a copy: a hook writes the values into a separate, pre-allocated tensor. The step stays one kernel. - No probe: the ordinary single kernel. Figure 4. One row per way of running layer 14. Squares: the 64 development prompts, filled where the input to layer 34 differs from "No probe". Detector: the model trained with the probe, on four diagnostic prompts, each giving the answer shown for "No probe", in two separate compilations . Bottom: one training run per row, in the row's color, from the original model with the recipe above, measured on the 64 development prompts, which are never trained on; the red and black runs end at step 1,024, the others continue to 3,072 with the optimizer restarted. The black run stays at 0 throughout. Shading: 95% intervals exact on the left, whole-prompt bootstrap on the right . The detector answers Yes exactly when layer 14 runs as two kernels, with or without a probe. The two compilations without a probe produce different logits on one of the four prompts 81% of vocabulary entries , and the detector answers No for both. Random changes to the user token's hidden state never make it answer Yes at one to three times the probe's change 0 of 256 ; at five times, 9 of 64 do. Run 049 https://github.com/SauersML/watching/blob/main/results/viewing-transfer-049 , protocol https://github.com/SauersML/watching/blob/main/experiments/activation-viewing-learning.md , random changes https://github.com/SauersML/watching/blob/main/results/head6-circuit-cpu . The probe is learned completely by step 640. Two kernels without a probe are learned more slowly: 50% of development prompts are answered correctly in both conditions at step 1,024, and 92.2% at the final continuation checkpoint step 3,072 , with a Yes − No log-odds separation of 58.8 nats. The copy is never learned. On 47 of its 64 development prompts the inputs to the trained layers are bit-identical with and without the probe, so no model can answer both correctly there; on the other 17, none is answered correctly in both conditions either. With "No probe" as the Yes condition, both answers see identical inputs and nothing is learned. Runs 051 and 052 https://github.com/SauersML/watching/blob/main/results/viewing-learning-051-052 , continuations 053 and 054r https://github.com/SauersML/watching/blob/main/results/viewing-continuation-053-054 , run 045 https://github.com/SauersML/watching/blob/main/results/detector-null-045 . We report four numbers: - Both answers correct: a prompt counts only if the model answers No without the condition and Yes with it, both as its top token over the whole vocabulary. - Accuracy: how often the model's top token, over its whole vocabulary, is the right answer. Always saying "Yes" gets 50%. - Correct direction: the share of prompts where turning the condition on raises the log-odds of the target answer over the other answer. A constant preference for one answer does not change it, so it shows whether the model detects the condition even when that preference makes the final answers wrong. - Separation: how far the probe moves the model, in nats: Animal choice uses owl and cat in place of Yes and No. - Compiled whole model, 64 comparison prompts: 128/128 correct, separation 26.03 nats 25.62, 26.41 , mean full-vocabulary loss 0.0000119 nats. Per-prompt results https://github.com/SauersML/watching/blob/main/results/two-block-scaled-034/fit/test 1024 wholegraph transfer.json , figure data https://github.com/SauersML/watching/blob/main/figures/snapshot 034.json . - Fresh process, 64 newly written prompts: the released checkpoint answers 128/128 correctly, separation 27.28 nats 27.06, 27.50 , mean loss 0.00000236 nats. The original model gets 64/128. Raw results https://github.com/SauersML/watching/blob/main/results/confirm-validation-038/fresh034 , figure https://github.com/SauersML/watching/blob/main/figures/fresh confirmation 034.png . The never-attached control run 045, Figure 4 repeats the detector's recipe exactly: same 256 training prompts, same eight prompts in every one of the 1,024 updates, same seed, learning rate and initialization. The only change is that the "Yes" examples receive the activations computed without the probe, so both labels see bit-identical inputs. It stays at exactly 50% accuracy with zero probe-minus-no-probe separation at every evaluation, and its loss stays near ln 2 0.697 nats on training, development and test prompts. The detector, trained on the real signal, reaches 100% on all three. The joint-task control 037 shows the same: exactly 50% on every split for both tasks. Detector control https://github.com/SauersML/watching/blob/main/results/detector-null-045 , joint control https://github.com/SauersML/watching/blob/main/results/joint-validation-036/null . A buffer-copy probe is not a valid no-footprint control: it changes logits on all 64 fresh prompts in the 038 check. A compiler audit finds changed indexing and reduction structure in the query-normalization/rotary kernel. Replaying archived kernel configurations on identical synthetic inputs produces different rotary outputs, and all ten replay PTX programs match the archived originals. Compiler evidence https://github.com/SauersML/watching/blob/main/docs/mechanism-audit.md . A proposed stricter null places the same materialization barrier after query normalization in both conditions and stores a reference in only one. It qualifies only if every input to the trainable tail—hidden states, masks, positions, rotary tensors—and every vocabulary logit match bit for bit. It has not been run yet. Protocol https://github.com/SauersML/watching/blob/main/docs/real-input-mechanism-protocol.md . The 034 audit independently checked that: - All 384 cached prompt pairs have matching masks, rotary inputs and positions; the conditions differ only in hidden states. - The raw answer logprobs reproduce the published gaps and losses. - Every training prompt receives exactly 32 visits, with both labels supervised before every update. - Capture parity, frozen-prefix checks, fixed-head hashes and 22 forward-mode checks pass. - The released weights and cache match the original artifact hashes. No leakage or scoring artifact was found. Audit https://github.com/SauersML/watching/blob/main/results/two-block-scaled-034/ADVERSARIAL REVIEW.md . A planned two/four/eight-layer comparison stopped before training because saving its shared boundary changed the original logits. Failure report https://github.com/SauersML/watching/blob/main/results/matched-depth-035 . One model learns two tasks. Detection targets are No/Yes. Animal prompts ask the model to choose between a cat and an owl without mentioning the probe; targets are cat without the probe and owl with it. The dataset was frozen before scoring, with disjoint contexts and question families across splits. | Split | Detection prompts | Animal prompts | Role | |---|---|---|---| | Training | 256 | 256 | Optimize the two native blocks | | Development | 32 | 32 | Track progress | | Final test | 64 | 64 | New contexts and question structures | | Challenge | 32 | 32 | Further formats, logic, corrections and languages | Training from the base model learns the signal but gives biased answers. Two seeds trained jointly for 2,048 updates raise the target answer's log-odds when the condition is on for 94.5% seed 1 and 99.2% seed 2 of new test prompts, but prefer one answer overall, so accuracy in the compiled whole model is 65.6% / 64.8% seed 1 and 76.6% / 71.1% seed 2 for detection / animal. Seed 1 https://github.com/SauersML/watching/blob/main/results/joint-validation-036 , seed 2 https://github.com/SauersML/watching/blob/main/results/confirm-validation-038 . Starting from the detector works. Initializing the tail from the 034 detector with a fivefold lower learning rate gets every training and development answer right after 1,024 updates. The compiled whole model answers 64/64 detection and 63/64 animal development prompts correctly. Run 039 https://github.com/SauersML/watching/blob/main/results/warm-joint-039 , protocol https://github.com/SauersML/watching/blob/main/experiments/warm-start-joint-pilot.md . Figure 5. The model trained on both tasks run 039 , evaluated in a new process on 64 newly written prompts per task, by its first output token. Capitalized “Cat” and “Owl” whose first token is “O” count as cat and owl. A new process evaluates the fixed 039 checkpoint on 128 prompts written and frozen before evaluation. Detection: 128/128 answers, 64/64 prompt pairs, separation 23.90 nats 23.21, 24.55 . Animal choice: 119/128 answers, 59/64 pairs, separation 23.68 nats 22.96, 24.38 . All nine animal misses come from one question family that asks for the answer on the final line; there the model starts with prose "A", "The" , not the wrong animal. The other seven families get 112/112. Per-prompt results and error tokens https://github.com/SauersML/watching/blob/main/results/joint-confirmation-040 , fresh prompts https://github.com/SauersML/watching/blob/main/data/confirmation v3 , protocol https://github.com/SauersML/watching/blob/main/experiments/fresh-joint-confirmation-040.md . Figure 6. Each row: chats of one layout, each run with and without the probe. New prompts: the detector detection and the joint model animal choice on 64 newly written single-message prompts per task, never trained on. Multi-turn chats and system message first: a model trained on four chat layouts one user message or a multi-turn chat, each with or without a system message first , on development chats whose contexts, questions and system messages were never trained on. Correct answers: the share of right answers Yes and owl with the probe, No and cat without ; dashed line: 50%, the score of giving the same answer either way. Right: the share of chats where turning the probe on raises the log-odds of the answer trained for the probe Yes; owl . Error bars: 95% intervals from resampling whole prompts. - New prompts and new multi-turn chats work. On newly written prompts, never trained on, the detector answers 128/128 and the joint model 128/128 and 119/128. A model trained on four chat layouts answers every new development chat correctly when the chat starts with a user message, including multi-turn histories. Run 046 https://github.com/SauersML/watching/blob/main/results/diverse-joint-046/final-step1024 . - Chats that start with a system message fail, although training included that layout. The model reached only 56–63% on its own system-first training chats, and new ones get 50%. The probe moves its answer there by only 0.03–0.16 nats, against about 23 nats with a user-first chat. The probe also changes the early system token's hidden state consistently, but in a direction almost unrelated to its change to the user token cosine similarity 0.03 . Prefix audit https://github.com/SauersML/watching/blob/main/results/prefix-formats-046 . Every prompt starts with the same chat prefix, <|im start| user , and attention is causal, so the user token's hidden state cannot depend on the question. It is therefore exactly the same vector in every prompt within a condition, and a different vector between conditions: across all 384 cached 034 prompts, the probe changes it by the same vector, of length 5.49, at the input to layer 34. The 576 cached 039 prompts show the same pattern with a different vector. <|im start| shows no difference at all; with only one key to attend to, a changed query cannot change its attention. 034 equality checks https://github.com/SauersML/watching/blob/main/results/tail-mechanism-041r/prefix signature.json , 039 equality checks https://github.com/SauersML/watching/blob/main/results/warm-joint-039/prefix signature.json , circuit-tracing design https://github.com/SauersML/watching/blob/main/docs/circuit-tracing.md . Figure 7. Each bar: how far one copy or one swap moves the Yes − No log-odds, as a share of the change from turning the probe on. Top: the prompt from Figure 8, where the probe moves it by 27.5 nats −13.25 to +14.25 ; copying user alone moves it 27.25. Bottom: mean of eight diagnostic test prompts 25.7 nats , checking all 64 heads of layers 34–35. Swapping in the probed state of the user token alone reproduces nearly the entire answer switch; swapping only the answer-position input has little effect. Causal audit and raw interventions https://github.com/SauersML/watching/blob/main/results/tail-mechanism-041r . Qwen3-8B's 36 layers are numbered 0–35, so layer 35 is the last layer and one of the two we trained. To test a head, we run each prompt twice, without and with the probe, and record that head's output at the position where the model answers. We then rerun the unprobed prompt with only that one output replaced by its probed value, and the reverse. Swapping layer 35, head 6 alone reproduces about 99% of the condition gap. Pasting its probed output into an unprobed run raises the Yes − No log-odds by 25.34 nats; pasting its unprobed output into a probed run lowers them by 25.41 nats. Both flip the actual answer on every tested prompt. No other head changes the log-odds by more than 0.23 nats. Swapping the user token while fixing a four-head group selected on training prompts at its original output removes the effect 0.00 and −0.03 nats , so the user signal reaches the answer through these heads. The trained layers separate the conditions by 25.69 nats on average, versus 0.047 for the original layers. Circuit measurements and raw interventions https://github.com/SauersML/watching/blob/main/results/tail-circuit-044 . Head 6 works as a switch on which token it reads. Without the probe, its query at the answer position attends to