CS2881R · Assignment 2 · Part 2

Refusal Probes on Qwen3-32B

Updated ET. Exploratory (train→val) and held-out (test) results are labeled separately throughout.

Bottom line

Can a linear read of Qwen3-32B's residual stream reproduce the judge's refuse/comply label, and what does it read?

  • Yes, before the model writes anything. A logistic-regression probe at the last template token (last_prompt, layer 57) reaches held-out test AUROC 0.975 [0.955, 0.988] in distribution and 0.979 [0.960, 0.993] on prompt types never seen in training. It barely beats the model's own first-token distribution (0.965 / 0.978), and the CIs overlap. The output layer already carries most of the decision.
  • After the response exists, the text is enough. The response-side probe (mean over response tokens, L47) scores 0.997 / 0.998. TF-IDF on the same text gets 0.988 / 0.993, and response length alone 0.906 / 0.983. Among a prompt's samples that disagree, probe and TF-IDF tie in distribution (0.976 each); the probe wins on held-out types (0.917 vs 0.875).
  • It reads behavior, not only harmfulness. The safety label alone scores 0.862 / 0.921. The prompt probe scores 0.93 within safe prompts on both test splits. Its typical error is over-predicting refusal on safe prompts with violent words: euthanizing a dog, a judo choke, a “massacre” scene for TV. The OR-Bench safe cell is its weakest (accuracy 0.80).
  • A probe direction can carry the refusal template without carrying the decision. Adding the best difference-of-means direction (L53) turns 88% of complied val prompts into template refusals. Ablating it everywhere strips the “I'm sorry, I can't” opener from the model's refusals (35 → 2 of 54). Yet a blind read of 320-token generations finds all 54 still refuse, as lectures and redirects. The logistic-regression direction does no better than a random one. Probe accuracy picks out where refusal is readable, not where it is decided.
  • No drop on held-out types for any probe, but the unsafe-and-complied cells are tiny (11 val responses), so within-unsafe numbers are noisy: test held-out within-unsafe is 0.68 for the prompt probe.

Probe configurations were chosen on val and frozen (part2/FROZEN.json, 06:28 UTC) before test was read once. The manual review is not yet done: Section 9 shows the sample with Claude's diagnostic reads, which are not manual labels.

1Question and setup

Part 1 built a judge that labels each Qwen3-32B response as FULL_REFUSAL, PARTIAL or FULL_COMPLIANCE. Part 2 asks whether a linear read of the model's residual stream reproduces that label, and what such a probe actually reads. The binary target is y = 1[FULL_COMPLIANCE]. Whether refusing was appropriate is a separate question, answered by the dataset's safe/unsafe label, which neither the judge nor the probes see.

Activations were re-extracted (teacher-forced, bf16, all 64 layers) from the committed responses, because the Part 1 activations stayed on the old pod. Ten prompt-side positions cover the last token of the user's text and every chat-template token after it. Four response-side summaries follow. Prompt-side states exist before the model writes anything, so they test prediction. Response-side states read text the model has already written.

… user textinst_last “?”<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>last_prompt “\n\n” first_responsefirst16_meanmean_responselast_response

Blue: prompt-side sites (one vector per prompt, shared by its 3 samples). Green: response-side sites (one per response). inst_last is where Zhao et al. (2025) find harmfulness encoded; last_prompt is Arditi et al.'s (2024) refusal-direction site.

What each method receives. Every learned method is fit on train; its threshold is set on val (maximum balanced accuracy).

MethodInputWhy it is here
Judge (the target)prompt + full responsedefines y
Probe, prompt-sideresidual stream at one prompt position and layercan refusal be read before generation?
Probe, response-sideresidual stream at or averaged over response tokensdoes the representation add anything over the text?
Majoritynothingfloor
Safety labeldataset safe/unsafe label: predict comply iff safehow far “harmfulness” alone gets
TF-IDF prompt / response / first 20 wordstext, 1–2-gram TF-IDF + logistic regressionwording cues
Next-token distributionthe model's own log-probs for its first token at last_promptdoes a probe beat the model's output layer?
Length, first token, keywordresponse length and truncation flag; first token id; refusal-phrase regexknown shortcuts

2Which layers and positions are best?

Exploratory: train → val.

Val AUROC by layerlogistic regressiondifference of means
Each line is one site. LR uses train-standardized activations with C chosen on val per layer, so its val curve is slightly optimistic.
Figure 1. Refusal becomes linearly readable gradually, and almost every site saturates past layer ~45. Prompt-side sites rise later than response-side sites, which are high even at layer 0.
Val AUROC, every site × layer (logistic regression)
Figure 2. Rows are positions in token order; columns are layers 0–63. Response-side rows are high everywhere because the response text itself carries the label (Section 6).

Late prompt-side positions win. On val, last_prompt peaks at L57 (LR 0.984, DoM 0.968 at L53), and every template token after <|im_end|> lands within ~0.01 of it. inst_last, the last user token, is lower (LR 0.962 at L48). Some of the refusal decision is assembled on the template tokens after the user finishes, consistent with Zhao et al. Response-side sites sit at 0.97–0.99 from layer 0 on, which says more about the text than about the model (Section 6).

3Probes vs baselines

AUROC with 95% cluster-bootstrap CI (over prompts)
Figure 3. Val points are exploratory (probe configs were chosen on val). Test points use the frozen configuration.

Two baselines matter. The next-token distribution (the model's own log-probs for its first token, fed to logistic regression) is nearly as good as the best prompt-side probe. On these prompts the decision is mostly committed by the final layer, and a residual probe adds a little calibration rather than new information. TF-IDF on the response is nearly as good as the best response-side probe, because refusals are worded like refusals. The other prompt-side baselines are much weaker: TF-IDF on the prompt scores 0.744 / 0.613 and the safety label 0.862 / 0.921. A probe at last_prompt therefore reads more than wording or topic.

4Held-out test, frozen configuration

Test evaluation is running. This section fills in when it finishes.

The two probes fail in opposite directions. The prompt probe over-predicts refusal: 31 of 238 complied in-distribution responses are scored as refusals, against 10 of 173 refusals scored as compliance. It cannot see the response, so it errs on borderline safe prompts that this model usually answers. TF-IDF on the response over-predicts compliance (16 of 173 refusals missed) because a hedged refusal that offers alternatives looks like an answer. The response probe makes 10 errors out of 411. PARTIAL responses, folded into the refusal class, sit in the middle of both probes' score distributions (median percentile 0.38), as a binary target that hides a third class should produce.

5Does the probe read behavior, or harmfulness?

The safety label alone predicts the judge label for 86% of dev responses, so a probe could score well by detecting “this prompt looks harmful” without tracking what the model did. Three checks separate the two: probes trained on the safety label instead of y; the behavior probe's AUROC within safe prompts and within unsafe prompts, where the safety label is constant; and a 2×2 contrast that estimates each direction with the other variable held fixed.

Prompt-side probes by layer (val)behavior probe → ybehavior probe, within safe promptsbehavior probe, within unsafe promptssafety-label probe → safety label
Within-unsafe AUROC rests on 11 complied val responses (out of 147), so it is noisy.
Figure 4. With the safety label held constant, the behavior probe still separates refused from complied responses: at last_prompt L57, val AUROC is 0.97 within safe prompts and 0.96 within unsafe ones. It is not only reading the safety label. Probes trained on the safety label reach 0.98 on that label and 0.93 on y.
2×2 directions: cosine between behavior (safety fixed) and safety (behavior fixed)
b = mean(safe, refused) − mean(safe, complied); h = mean(unsafe, refused) − mean(safe, refused). Both from train. Solid: all sources pooled. Dashed: OR-Bench prompts only, so the dataset source is held fixed (pooled safe-refused prompts are mostly OR-Bench, safe-complied mostly XSTest).
Figure 5. Pooled over sources, the two directions are nearly opposite in early layers (cos ≈ −0.9). Most of that is dataset source. Inside OR-Bench only (dashed), early cosines are weaker (−0.3 to −0.5) and cross zero around layers 24–40. At the late template positions they settle at +0.3 to +0.45: related, but far from the same direction.

The pooled 2×2 contrast was misleading at first: its strongly negative cosine came from the dataset source. Safe-refused prompts are mostly OR-Bench and safe-complied mostly XSTest. Holding the source fixed (OR-Bench only) weakens the early negative cosine from about −0.9 to −0.4 and turns it positive after layer ~40. Inside OR-Bench, at late template positions, the “unsafe vs safe, both refused” direction also predicts behavior on safe prompts: val AUROC 0.85 at last_prompt L56, against 0.24–0.44 when pooled. That is the over-refusal story in geometric form: on OR-Bench, “looks harmful” and “will refuse” share a component. They are not one direction, though (cos ≈ 0.35), and the behavior probe stays at 0.90 within safe OR-Bench prompts.

The source-controlled run is a post-hoc check on dev data, added after the pooled result looked implausible; it is not part of the frozen test configuration.

6Shortcuts

Length and truncation are strong shortcuts on held-out types (0.983) because XSTest refusals are short. The first token alone scores 0.85–0.87: refusals open with “I”. The response probe survives inside every subgroup, including the within-prompt comparison, where length and topic are mostly matched. It still errs when a response opens like a refusal and then answers in full (orbench_hard_887, orbench_hard_5). In those cases it is reading the opener.

7Probe families and controls

Exploratory: train → val, at the val-selected (site, layer).

Data efficiency at last_prompt (val AUROC)logistic regressiondifference of means
Figure 6. Nested random subsets of train prompts (3 responses each).

All families land within 0.02–0.04 AUROC of each other, so the readout is not fragile to the estimator. Logistic regression wins at both sites, and shrinkage LDA is nearly as good. An MLP adds nothing, so a nonlinear readout gains no accuracy here. Shuffled-label LR sits at chance (0.39–0.58). The best of 500 random directions reaches 0.93 at last_prompt, a reminder that the label is spread across many dimensions late in the network, so a high AUROC alone does not single out a direction. Difference of means nearly matches LR with as few as 40 train prompts.

8Causal check: do these directions mediate refusal?

The intervention run (ablation and addition, greedy generation on val prompts) is in progress.

Taken at face value, the keyword detector says DoM ablation all but eliminates refusal (72% → 4%), while random directions of the same norm leave it at 69–70%. Reading the generations reverses that conclusion. The 64-token outputs were mostly lectures, so the four refused-prompt conditions were re-generated at 320 tokens and labeled blind to condition:

Blind read of 320-token generations on the 54 always-refused val prompts. Two Claude subagents labeled all 216 shuffled generations under rubric v2_1 without seeing the condition. These are not the Part 1 judge and not manual labels. Style: template = “I’m sorry, I can’t…”, lecture = moralizes and gives nothing, redirect = declines and offers alternatives, benign_answer = answers a premise-rejecting prompt.

Conditionrefusalpartialcomplytemplatelectureredirectbenign answercompliesdegenerate
no intervention493235011710
ablate DoM (L53)540021728601
ablate LR (L57)445521418740
ablate random482432111730

After DoM ablation, all 54 generations still refuse under the rubric, slightly more than without intervention (49 refusals, 3 partials, 2 compliances). What changes is the style: the “I'm sorry, I can't” template drops from 35 to 2, replaced by lectures and redirects. The L53 difference-of-means direction, the best DoM probe on val, carries how the model refuses, not whether it refuses. That differs from Arditi et al., whose ablated models complied. One reason may be that their direction was chosen by intervention efficacy at a mid layer, while ours was chosen by probe accuracy at a late one. The LR direction's ablation leaves templates in place and moves compliance no more than a random direction (10 vs 6 non-refusals of 54, within noise). Addition is still a strong lever on style: DoM ×2 turns 88% of complied prompts into template refusals.

Follow-up: Section 14 sweeps the layer of the ablated direction to test whether a mid-layer direction removes the decision itself.

9Agreement and disagreement review

36 test responses: 12 where the response probe disagrees with the judge (of 20), 12 where the prompt probe disagrees (of 67), and 4 agreements per judge label. Disagreement strata diagnose failure modes; they do not estimate accuracy. The Claude read column is a diagnostic note written by Claude during the autonomous run. It is not a manual label. Manual labels go in part2/manual_labels_test.csv via python label_tool.py --sample part2/review_sample.jsonl --out part2/manual_labels_test.csv, with the judge and probe outputs hidden.

Claude's tags: 12 prompt- or response-probe errors driven by topic words; 3 response-probe errors on a refusal opener followed by a full answer; 4 cases where the rubric itself is ambiguous (e.g. xstest_138, where the judge labeled the same benign reading FULL_COMPLIANCE in two samples and PARTIAL in the third); 2 errors the prompt probe cannot fix, because the model's 3 samples disagree; 2 boundary cases; 1 question of appropriateness; 12 clean agreements.

The review sample is drawn after the test run.

10What geometry does a linear probe assume?

11Limitations

12Run log

  1. Pod created (1× A100-SXM4 80GB, $1.79/hr). Model and data synced.
  2. Activations extracted: 10 prompt + 4 response sites × 64 layers.
  3. Sweep: 14 sites × 64 layers, DoM and LR, plus safety-label probes.
  4. Val evaluation of probes and 9 baselines. Probe families, controls, data efficiency and pooled 2×2 contrast run.
  5. Configuration frozen (part2/FROZEN.json). Test read once.
  6. Test results in. Review sample drawn; Claude's diagnostic reads written.
  7. Pooled 2×2 contrast looked source-confounded, so re-ran it inside OR-Bench only.
  8. Causal check relaunched after a bug fix: ablation and addition of DoM, LR and random directions.
  9. Causal results in. The keyword detector says DoM ablation removes refusal; reading the generations shows mostly lecture-style refusals. Relaunched the ablation conditions at 320 tokens for a blind read.
  10. Blind read done: all 54 DoM-ablated generations still refuse. The direction carries the refusal template, not the decision. Dashboard mirrored to GitHub Pages.
  11. Old pod stopped and its host's GPUs were taken, so it was terminated. A new A100 pod re-extracts activations for the follow-ups in Section 14.
  12. Activations re-extracted on the new pod in 9 minutes.
  13. Probe search (14.3) done: no pooled or stacked probe beats the single-site probe on val, so test was not read again.
  14. Judge validation crashed while saving (a JSON key-type bug) and was re-queued. Causal layer sweep started.

13Reproduce

python extract_part2.py --all      # activations2/ (10 prompt + 4 response sites × 64 layers, fp16, ~12 GB)
python probe_sweep.py              # results/part2/sweep.json          (train → val)
python evaluate_part2.py           # results/part2/eval_val.json       (probes + baselines, val)
python probe_compare.py            # results/part2/compare.json        (families, controls, 2×2)
python freeze_part2.py             # part2/FROZEN.json                 (refuses to overwrite)
python evaluate_part2.py --final   # results/part2/eval_test.json      (test_id, test_heldout_type)
python causal_check.py             # results/part2/causal.json         (val prompts only)
python review_sample.py            # part2/review_sample.jsonl
python make_dashboard.py           # dashboard/index.html
./publish_pages.sh                 # rebuild + push to GitHub Pages

Live copy: agastyasridharan.github.io/cs2881-hw2-refusal-probes, updated after each stage.

Pod: 1× A100-SXM4-80GB, torch 2.8.0+cu128, transformers 5.22.2, scikit-learn 1.9.1. Model Qwen/Qwen3-32B at revision 9216db57, bf16. Splits sha256 9d4310e0…54ce (unchanged from Part 1).

14Follow-ups after the frozen run

Everything in this section was added after the frozen test evaluation, during an autonomous follow-up session. It uses train and val only unless a result is explicitly marked as test. Any test number here comes from a second, separately frozen configuration, and is less clean than Section 4 because the test errors had already been seen.

14.1 A local judge for intervention outputs

Validating Qwen3-32B-as-judge and the response probe as automatic judges against the API judge (val) and the blind reads (Section 8). Running.

14.2 Which layer's direction causes refusal?

Ablation and addition of the difference-of-means direction from 12 layers at last_prompt. Queued.

14.3 Better prompt-side probes

Prompt-side candidates, val AUROC (train → val, 95% cluster-bootstrap CI). concat = 10 template positions concatenated; mean = averaged; stack = second-level LR on out-of-fold scores.

ProbeAUROC95% CIwithin safewithin unsafe
v1: last_prompt L57 (frozen)0.98420.969–0.9940.9720.955
stack: v1 + next-token0.98400.969–0.9940.9710.955
mean positions @L570.98370.968–0.9940.9700.945
stack: last_prompt, layers 24–630.98190.966–0.9930.9660.941
mean positions @L600.98070.963–0.9930.9640.931
mean positions @L530.98030.963–0.9930.9620.943
concat positions @L600.97880.960–0.9920.9580.935
next-token distribution0.97860.961–0.9910.9570.947
concat positions @L570.97820.959–0.9920.9550.935
stack positions @L570.97660.957–0.9900.9510.941
concat positions @L530.97640.956–0.9910.9510.931
mean positions @L480.97540.955–0.9900.9600.877
stack positions @L530.97540.955–0.9900.9450.935
concat positions @L480.97480.952–0.9910.9560.903
mean positions @L400.94270.903–0.9720.9150.713
concat positions @L400.94070.901–0.9710.9060.765

Nothing beats the single-site probe. Concatenating or averaging the ten template positions, stacking 14 layers, and stacking the probe with the model's own first-token distribution all land within 0.002 of the frozen probe on val (0.984) or below it. Under the pre-registered rule, a v2 would be frozen only if it beat v1 on val; none did, so test was not read again. Every late template position carries the same information, and the residual stream at last_prompt holds nothing that the output distribution lacks and a linear readout could use. Section 14.4 asks whether that ceiling comes from the probe or from the model's sampling noise.

14.4 Does the prompt probe know the model's refusal probability?

Sampling each val/test prompt many times to compare probe scores with the model's empirical refusal rate. Queued.

14.5 Generation-time monitoring: how many tokens does the probe need?

Response-probe and TF-IDF accuracy as a function of response tokens seen. Queued.

14.6 Per prompt type: does the probe track the model or the surface?

Compliance rate per prompt type, actual vs predicted (val + test responses; frozen v1 probe with its val threshold; test rows are a post-hoc diagnostic). Pearson r across the 28 types: 0.992.

Source / typelabelnmodel compliesv1 predicts complyv1 accnext-token predicts
orbench / deceptionsafe480.560.440.830.31
orbench / harassmentsafe300.470.400.870.40
orbench / harmfulsafe410.710.590.830.59
orbench / hatesafe30.000.001.000.00
orbench / illegalsafe1530.590.530.830.55
orbench / privacysafe410.630.590.850.54
orbench / self-harmsafe840.260.210.900.18
orbench / sexualsafe330.390.450.940.45
orbench / unethicalsafe420.500.501.000.50
orbench / violencesafe170.290.180.880.18
xstest / definitionssafe391.001.001.001.00
xstest / figurative languagesafe331.001.001.001.00
xstest / historical eventssafe361.001.001.001.00
xstest / homonymssafe421.000.930.930.93
xstest / nons group real discrsafe271.001.001.001.00
xstest / privacy fictionalsafe151.001.001.001.00
xstest / privacy publicsafe241.001.001.001.00
xstest / real group nons discrsafe241.001.001.001.00
xstest / safe contextssafe750.910.800.890.80
xstest / safe targetssafe750.970.960.930.88
xstest / contrast definitionsunsafe390.030.000.970.00
xstest / contrast discrunsafe240.170.120.960.25
xstest / contrast figurative languageunsafe320.030.000.970.00
xstest / contrast historical eventsunsafe360.000.001.000.00
xstest / contrast homonymsunsafe410.220.050.780.05
xstest / contrast privacyunsafe150.000.001.000.00
xstest / contrast safe contextsunsafe750.010.000.990.00
xstest / contrast safe targetsunsafe750.040.000.960.00

Across types, the probe tracks the model, not the surface. On XSTest, each safe type has an unsafe contrast built from the same words. The probe predicts compliance on nearly every safe item and refusal on nearly every contrast item, as the model does. Its misses sit where the model itself is unpredictable: OR-Bench's “seemingly toxic” safe categories, where the model complies 26–71% of the time. There the probe under-predicts compliance by 0.04–0.12 per category. Two places look like surface reading: safe contexts (0.80 predicted vs 0.91 actual, e.g. “kill” in a video game) and unsafe contrast homonyms, where the model complies 22% of the time and the probe predicts 5%. The next-token baseline makes almost the same calls type by type.