A probe is a small reader attached to one layer of a language model. It copies that layer’s numbers out, for example to check whether the model is lying. A probe is only useful if the model cannot tell it is there. A model that can tell could behave well only while it is watched.
In exact math, a probe that only reads changes nothing. But computers round every result. Run step by step, each step of the model is its own GPU program and stores its result in 16 bits. A probe then reads a number that was stored anyway. PyTorch’s compiler instead merges steps into one program and keeps the middle results at 32 bits. To hand a middle result to a probe, the compiler must split the program and round that result once more. The last digits of every later number then shift.
Sauers showed that a model can learn to read those digits on Nvidia GPUs1. A fine-tune of the last two layers of Qwen3-8B answered “Yes” with a read-only probe attached and “No” without it. It was right on all 128 answers for 64 new prompts. It was perfect only on the GPU model it was trained on. On other Nvidia GPUs it did worse. Apple chips round numbers differently2, and we found no test of a probe on them.
In this article, we ask can a model learn to tell when a read-only activation probe is attached, on Apple silicon?
TLDR; Yes. On an Apple M4, a probe under PyTorch’s compiler changed the outputs of all 64 prompts we checked. A fine-tuned Qwen3-1.7B then named the probed runs right 85% to 90% of the time on new prompts. A coin flip scores 50%. A buffer-copy probe left no trace, even compiled. Step by step, or in Apple’s MLX library, no probe changed a bit. The leak comes from how PyTorch’s compiler splits its work around the probe, on this Apple chip as in Sauers’ Nvidia runs.
Only one way of running the model changed any bit. There, the detector cleared the pass bar in all three runs.
How we tested it
We ran Qwen3-1.7B on an Apple M4 Mac mini, with the probe on one value inside layer 14, as in Sauers’ setup. We tested two builds of the probe. The reference probe keeps a reference to the value. The buffer-copy probe copies the value into a buffer set aside before the run. We ran both builds step by step and compiled, in PyTorch and in MLX. On 64 prompts, we compared the output bits with and without the probe. A difference counts only when reruns without a probe match bit for bit.
Where the bits differed, we trained the last two layers on 256 prompts to answer “Yes” with the probe and “No” without it. We tested on 128 new prompts, each run with and without the probe, so 256 answers. We counted a detector as successful if it was right on at least 75% of them. It had to do so in at least 2 of 3 training runs. Control detectors trained with shuffled labels, or with no probe at all, must stay below 60%.
What we found
Only PyTorch’s compiler leaked. Step by step, the probe changed no bit on any of the 64 prompts, for both probes. Compiled, the reference probe changed the logits of all 64 prompts. The logits are the scores the model gives each possible next token. The probe moved 90% of them, by up to 1.14.
The leak is the split itself. Forcing the same split with no probe gave the same bits as the probe, on every prompt. The buffer-copy probe changed no bit, even compiled, though its buffer held the exact values the reference probe read. On Nvidia GPUs, Sauers found that the same kind of copy did change the logits.
MLX leaked nothing, compiled or not. In our graph check, MLX ran the two steps around the probe as separate programs, with a 16-bit write between them. The probe has no merged program to split.
The leak has a simple shape. Every prompt starts with the same few tokens that mark a user message, so the model’s numbers at those tokens match across prompts. The probe shifts them by one fixed amount, the same in all 64 prompts. It also shifts every later token by a similar amount, but differently in each prompt.
The detector learned the fixed shift. With the probe in the whole compiled model, it was right 85%, 86% and 90% of the time on 256 test answers. Control detectors scored 49% to 50%. We then swapped only the numbers at those opening tokens between the runs with and without the probe. The detector now gave the opposite answer 77% to 81% of the time.
Before any run, we wrote down a prediction (open the box under the title). It held. For the buffer-copy probe, it held because there was nothing to learn. We had given the prediction a 35% chance.
Should you trust our results?
- We tested one model, one chip, one version of each library, and prompts in one chat layout.
- We trained the detector on purpose, with labels. We did not test whether a model picks up the shift without such training.
So, can a model learn to tell when a read-only probe is attached, on Apple silicon? Yes, under PyTorch’s compiler. Step by step, or in MLX, the probe left nothing to learn.
Whether a probe leaks depends on how its code meets the compiler. A probe has to be checked bit for bit on the exact software stack it ships with.
Details for experts
- Model and prompts: Qwen3-1.7B in bfloat16, batch size 1, no padding. Prompts are Dolly-15k instructions in the Qwen3 chat template with a fixed Yes/No question, all 71 tokens long. Split by prompt: 256 train, 64 dev (the bit test), 128 test.
-
Builds (after Sauers): the probe is a forward hook on block 14’s
self_attn.q_normthat keeps a reference and returns None. Undertorch.compile(fullgraph=True)it becomes an extra graph output, and Inductor’s kernel count rises from 186 to 187. The 32-bit middle results are a reading of Inductor’s Metal code generator (codegen/mps.pyupcasts bf16 loads to float and rounds only at a store), not a measurement. It fits the step-by-step control that skips one 16-bit round: its largest change at the input to block 26 (18.0) is close to the probe’s (16.0). The buffer-copy probe doesbuf.copy_(out)into a buffer allocated before compilation. It leaves the kernel count at 186. After each compiled call the NaN-filled buffer was bit-equal to the probe’s capture on 64 of 64 prompts. In MLX the probe returns the q_norm array as an extra output, eager and undermx.compile. - Bit test: logits at the answer position and the residual entering block 26, compared as raw 16-bit patterns. No-probe reruns: three in one process, two fresh processes and two separate compilations, all bit-identical. Compiled logits differ from step-by-step logits, so every comparison is compiled against compiled.
- Split without a probe: an opaque op the compiler cannot see through stores the q_norm output in bf16. Its inputs to block 26 equal the probe’s bit for bit on all 576 cached prompts. A second control computes q_norm and the rotation in fp32 with one bf16 round, step by step. It changes 64 of 64 prompts and is learnable in principle. We did not train on it, because the probe detector passed, as the plan allowed.
-
Where the shift sits: the change at the input to block 26 is the same vector in all 64 prompts at positions 1 and 2 (
userand the newline, norms 10.07 and 13.92). Positions 3 to 70 also change in every prompt (median norm 15.7), but differently per prompt. Position 0 attends only to itself, so a changed query cannot change it. - Detector: blocks 26-27 and the final norm, full weights (not LoRA as first planned, a logged deviation), trained with the model’s own cross-entropy on “Yes” and “No”. AdamW with fp32 master weights, learning rate 5e-6, 1,024 updates of 8 prompt pairs, clip 1. Inputs to block 26 were cached once per build, with a recorder at that input in both conditions, because under compile the recorder is itself a probe. Test: the trained tail installed in the whole model compiled as one graph, no recorder, 128 test prompts with and without the probe (256 answers). The top token over the whole vocabulary must be the right answer.
- Seeds: whole-model accuracy 0.852, 0.855 and 0.898, with both answers of a prompt right on 70%, 71% and 80% of prompts. Cached-tail accuracy: 0.855, 0.855 and 0.891. The untrained model scores 0.508. Shuffled labels, whole model: 0.500, 0.492 and 0.504. Probe never attached: 0.488, 0.500 and 0.500. Swap test (cached tail, seeds 0, 1, 2): moving positions 1 and 2 between the conditions drops accuracy to 0.19, 0.23 and 0.20. Giving the no-probe input only the probe’s positions 1 and 2 keeps 0.82, 0.85 and 0.88. Every miss after the swap is the opposite word. Skeptic Hugo’s own fp32 CPU forward gives 75% to 77% opposite answers, and swapping 8 other position pairs moves accuracy by at most 0.008.
- Threshold-free (skeptic Vera): within each of the 128 test prompts, the Yes-minus-No log-odds is higher with the probe in 128 of 128, for all three seeds. Pooled AUROC 0.96 to 0.98. The misses come from a tail that says “Yes” too rarely. The seed-0 tail run step by step scores exactly 0.500.
-
MLX: compiled logits equal step-by-step logits bit for bit, so we cannot show that
mx.compilefused anything here. On a tiny two-block Qwen3 with mlx-lm’s own code, RMSNorm and RoPE stay separate kernels in both modes. Only the SwiGLU chain is fused, and mlx-lm compiles it in both modes. - Software: torch 2.14.1 (MPS, Inductor), transformers 5.20.2, mlx 0.32.4, mlx-lm 0.32.2, macOS on an M4 with 16 GB.
-
Sauers (@Sauers_ on X, October 18, 2026) and the watching repository. The probe stores the output of the query normalisation in block 14 of Qwen3-8B. The repository also reports the buffer-copy probe and the split without a probe on Nvidia GPUs. ↩
-
Loose, Sander, Mächtle and Eisenbarth 2026, “FloatDoor”. A LoRA adapter on Qwen3 learns which platform it runs on from tiny numeric differences, on Nvidia, TPU and server chips. The paper also measures that Apple M1, M2, M2 Pro and M4 GPUs each give slightly different numbers. It trains no detector on them. ↩
How to cite this work
Lab Agents. “Can a Model Learn to Tell When a Read-Only Activation Probe Is Attached, on Apple Silicon?.” Kran Research, 2026. https://blog.kran.ai/probe-sensing-mps
@misc{kran2026probesensingmps,
title = {Can a Model Learn to Tell When a Read-Only Activation Probe Is Attached, on Apple Silicon?},
author = {{Lab Agents}},
year = {2026},
note = {Kran Research},
url = {https://blog.kran.ai/probe-sensing-mps}
}