contrastset 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- contrastset-0.1.0/LICENSE +21 -0
- contrastset-0.1.0/PKG-INFO +365 -0
- contrastset-0.1.0/README.md +335 -0
- contrastset-0.1.0/pyproject.toml +105 -0
- contrastset-0.1.0/pyproject.toml.orig +80 -0
- contrastset-0.1.0/src/contrastset/__init__.py +3 -0
- contrastset-0.1.0/src/contrastset/adapters/__init__.py +14 -0
- contrastset-0.1.0/src/contrastset/adapters/anthropic.py +63 -0
- contrastset-0.1.0/src/contrastset/adapters/base.py +78 -0
- contrastset-0.1.0/src/contrastset/adapters/fake.py +60 -0
- contrastset-0.1.0/src/contrastset/adapters/hf.py +457 -0
- contrastset-0.1.0/src/contrastset/adapters/ollama.py +93 -0
- contrastset-0.1.0/src/contrastset/adapters/openai.py +76 -0
- contrastset-0.1.0/src/contrastset/checks/__init__.py +145 -0
- contrastset-0.1.0/src/contrastset/checks/model.py +133 -0
- contrastset-0.1.0/src/contrastset/checks/surface.py +199 -0
- contrastset-0.1.0/src/contrastset/cli.py +358 -0
- contrastset-0.1.0/src/contrastset/generate.py +544 -0
- contrastset-0.1.0/src/contrastset/grade.py +633 -0
- contrastset-0.1.0/src/contrastset/jsonl.py +46 -0
- contrastset-0.1.0/src/contrastset/py.typed +0 -0
- contrastset-0.1.0/src/contrastset/records.py +642 -0
- contrastset-0.1.0/src/contrastset/templates/README.md +24 -0
- contrastset-0.1.0/src/contrastset/templates/qwen3-nothink.txt +7 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Misera
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contrastset
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Build and check contrast sets (matched stimulus pairs) for interpretability experiments on local LLMs.
|
|
5
|
+
Keywords: interpretability,llm,contrast sets,probing,datasets
|
|
6
|
+
Author: Thomas Misera
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: pydantic>=2.7
|
|
19
|
+
Requires-Dist: httpx>=0.27
|
|
20
|
+
Requires-Dist: pyyaml>=6
|
|
21
|
+
Requires-Dist: anthropic>=2.3.0 ; extra == 'anthropic'
|
|
22
|
+
Requires-Dist: torch>=2.14.1 ; extra == 'hf'
|
|
23
|
+
Requires-Dist: transformers>=5.21.0 ; extra == 'hf'
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Project-URL: Homepage, https://github.com/modemuser/contrastset
|
|
26
|
+
Project-URL: Repository, https://github.com/modemuser/contrastset
|
|
27
|
+
Provides-Extra: anthropic
|
|
28
|
+
Provides-Extra: hf
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# contrastset
|
|
32
|
+
|
|
33
|
+
> **Status: v0.1.0.** Used for one experiment so far (an "I don't know" readout in
|
|
34
|
+
> Qwen3-8B). The file formats and the CLI may still change.
|
|
35
|
+
|
|
36
|
+
Build and check **contrast sets**, matched pairs of texts that differ in exactly one
|
|
37
|
+
controlled variable, for interpretability experiments on local LLMs (large language
|
|
38
|
+
models). A probe (a small classifier trained on a model's internal activations)
|
|
39
|
+
finds whatever separates the two sides of your data. If the sides also differ in
|
|
40
|
+
length, wording, or how famous the names are, a probe trained on those pairs learns
|
|
41
|
+
those surface cues instead of the variable you care about, and its accuracy looks
|
|
42
|
+
like a result. Matched pairs remove the alternatives by construction: the two sides
|
|
43
|
+
share everything but the contrast, so whatever separates them is the contrast.
|
|
44
|
+
contrastset makes such pairs cheap to produce and checks that they differ in one
|
|
45
|
+
thing only.
|
|
46
|
+
|
|
47
|
+
Three commands, each useful on its own, sharing one file format: JSONL (JSON Lines,
|
|
48
|
+
one JSON object per line), readable by eye and with `jq`. Data goes to stdout,
|
|
49
|
+
progress and reports to stderr, so every command pipes.
|
|
50
|
+
|
|
51
|
+
| command | reads | writes |
|
|
52
|
+
|------------|-------------------------------------|---------------------------|
|
|
53
|
+
| `grade` | entities with questions and answers | what the subject model knows, with every raw sample |
|
|
54
|
+
| `generate` | a spec (+ a graded file, if used) | pairs |
|
|
55
|
+
| `check` | any pair file (`a` and `b` per line) | per-pair verdicts, exit code for CI |
|
|
56
|
+
|
|
57
|
+
Two models are involved and never confused: the **subject model** is the small
|
|
58
|
+
local model being studied (graded behaviourally, by its answers, never by asking it
|
|
59
|
+
whether it knows); the **generator model** writes sentence frames and never sees
|
|
60
|
+
what the subject knows.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install "contrastset @ git+https://github.com/modemuser/contrastset@v0.1.0" # core: grade/generate/check over HTTP
|
|
64
|
+
pip install "contrastset[hf] @ git+https://github.com/modemuser/contrastset@v0.1.0" # + in-process transformers, log-prob grading
|
|
65
|
+
contrastset --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Python 3.11+. Extras: `hf` (torch, transformers), `anthropic` (a generator model
|
|
69
|
+
through the Anthropic API). The core needs neither: Ollama and any
|
|
70
|
+
OpenAI-compatible server are reached over HTTP. For development: `uv sync`, then
|
|
71
|
+
`uv run pytest`.
|
|
72
|
+
|
|
73
|
+
## The ladder
|
|
74
|
+
|
|
75
|
+
Each example adds one concept. Every example folder holds its inputs and its
|
|
76
|
+
committed outputs, so you can read the result of each step without running it.
|
|
77
|
+
Commands are run from inside the example folder. The committed files were made
|
|
78
|
+
with `SOURCE_DATE_EPOCH=1793145600` (timestamps fixed), so rerunning a
|
|
79
|
+
deterministic step reproduces them byte for byte.
|
|
80
|
+
|
|
81
|
+
### 00 · Marks & Tegmark: does your probe stage work at all?
|
|
82
|
+
|
|
83
|
+
Not a contrastset feature but a check on the stage after it: a difference-of-means
|
|
84
|
+
probe on Marks and Tegmark's true/false statements, on Qwen3-8B, with a frozen split
|
|
85
|
+
and the expected per-layer AUC. If your probe code can't reproduce this, a null
|
|
86
|
+
result on your own pairs means nothing. See
|
|
87
|
+
[`examples/00-marks-tegmark/README.md`](examples/00-marks-tegmark/README.md); it is
|
|
88
|
+
also the opt-in smoke test (see Testing).
|
|
89
|
+
|
|
90
|
+
### 01 · lexical control: spec, inline slots, hand-written frame, `check`
|
|
91
|
+
|
|
92
|
+
No model, no graded file, about a second.
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
cd examples/01-lexical-control
|
|
96
|
+
contrastset generate spec.yaml > pairs.jsonl
|
|
97
|
+
contrastset check pairs.jsonl --spec spec.yaml > check.jsonl
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
A **spec** says what the contrast is. Here, two inline word lists fill one
|
|
101
|
+
placeholder, `[[X1]]`, in hand-written **frames**:
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
frames:
|
|
105
|
+
- The [[X1]] is next to the window.
|
|
106
|
+
slots:
|
|
107
|
+
select:
|
|
108
|
+
a: {source: inline, values: [table, lamp, chair]}
|
|
109
|
+
b: {source: inline, values: [shelf, sofa, desk]}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Each pair fills the same frame twice, once with a value for side a and once for
|
|
113
|
+
side b, so the sides differ only in the slot, by construction. A probe that tells
|
|
114
|
+
these pairs apart is reading word identity: run it as a control next to every real
|
|
115
|
+
contrast set. `check` runs the checks the spec lists (here `length`, `overlap`) and
|
|
116
|
+
exits non-zero if any pair fails.
|
|
117
|
+
|
|
118
|
+
### 02 · capitals: `grade`, then select slots from what the model knows
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
cd examples/02-capitals
|
|
122
|
+
contrastset grade entities.jsonl --model qwen3:14b --template template.txt > graded.jsonl
|
|
123
|
+
contrastset generate spec.yaml --graded graded.jsonl > pairs.jsonl
|
|
124
|
+
contrastset check pairs.jsonl --spec spec.yaml > check.jsonl
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Entities** are what you ask about, with reference answers (any alias counts):
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{"entity": "Burundi", "type": "country", "questions": [{"id": "capital", "q": "What is the capital of Burundi?", "answers": ["Gitega"]}]}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`grade` asks every question 8 times at temperature 0.7 plus once greedily (always
|
|
134
|
+
the most likely token), matches the answers against the references and sorts each
|
|
135
|
+
(entity, question) into a **bin**: `known`, `confidently_wrong` (the same wrong
|
|
136
|
+
answer again and again), `unstable` (scattered: guessing) or `refused`. Every raw
|
|
137
|
+
sample is kept.
|
|
138
|
+
|
|
139
|
+
The **template** is the exact text the model sees, chat format included; the
|
|
140
|
+
adapters send it as-is. It is always your choice (`--template` is required and has
|
|
141
|
+
no default) and carries the refusal phrases written for its own wording:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
---
|
|
145
|
+
refusal_phrases: ["i don't know", "i do not know", "not sure", ...]
|
|
146
|
+
---
|
|
147
|
+
<|im_start|>user
|
|
148
|
+
{{question}} Answer with just the city name. If you do not know, say "I don't know".<|im_end|>
|
|
149
|
+
...
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A **graded slot** selects candidates from the graded file. The unit is an (entity,
|
|
153
|
+
question) pair, and selectors filter a flattened view of it. To see that view,
|
|
154
|
+
print it, since it is itself a valid slot file:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
contrastset generate --candidates --graded graded.jsonl | jq -c 'select(.bin == "known")' | head -2
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Fields: `value` (= `entity`), `question`, `bin`, `correct_rate`, `consistency`,
|
|
161
|
+
`refusal_rate`, `n_correct`, `n`, `type`, `entity_bin`, `q.<id>` (the bin of the
|
|
162
|
+
entity's other questions), `answer.<id>`, `meta.<key>`. Filter in the spec with
|
|
163
|
+
`where` (`{bin: known}`, `{correct_rate: {max: 0.125}}`), or with jq and feed the
|
|
164
|
+
result back as `{source: file, path: ...}`: selectors are a convenience, not a
|
|
165
|
+
requirement.
|
|
166
|
+
|
|
167
|
+
This example doubles as a sanity check of the matcher: a 14B model knows nearly
|
|
168
|
+
every capital, so many `unstable` answers would mean the matcher, not the model, is
|
|
169
|
+
broken. qwen3:14b knows 34 of 37; the three it doesn't are the classic confusions
|
|
170
|
+
(Burundi → "Bujumbura", the capital until 2019; Sri Lanka → "Colombo"; Côte
|
|
171
|
+
d'Ivoire split between Abidjan and Yamoussoukro). With only three unknown
|
|
172
|
+
countries, every b side recurs about 17 times, and `check` says so: `duplicates`
|
|
173
|
+
warns on all 102 pairs. Read the warnings as information about your data, not
|
|
174
|
+
noise.
|
|
175
|
+
|
|
176
|
+
### 03 · two-clause rivers: sides, familiar entities, clause order, factors, behaviour
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
cd examples/03-two-clause-rivers
|
|
180
|
+
contrastset grade entities.jsonl --model qwen3:14b --template template.txt > graded.jsonl
|
|
181
|
+
contrastset generate spec.yaml --graded graded.jsonl --adapter anthropic --model claude-haiku-5-5 --limit 60 > pairs.jsonl
|
|
182
|
+
contrastset check pairs.jsonl --spec spec.yaml > check.jsonl # add the subject flags below for `behaviour`
|
|
183
|
+
contrastset check pairs.jsonl --spec spec.yaml --checks behaviour --require behaviour \
|
|
184
|
+
--graded graded.jsonl --subject-model qwen3:14b --template template.txt > check-behaviour.jsonl
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Two-clause questions: side a asks about two towns whose river the model knows,
|
|
188
|
+
side b swaps one for a town it doesn't. New concepts:
|
|
189
|
+
|
|
190
|
+
- **`sides`** say which slots go where: `a: [shared, known]`, `b: [shared, unknown]`.
|
|
191
|
+
A slot on both sides keeps its value.
|
|
192
|
+
- **`slot_defaults: {familiar: true}`**: every town must have its other questions
|
|
193
|
+
(country, region) known, so the entity is familiar and only the river fact varies.
|
|
194
|
+
- **`order: both`** also emits every pair with the clauses reversed (`rotations`
|
|
195
|
+
cycles through every position).
|
|
196
|
+
- **`factors`** control a variable instead of leaving it to chance: here whether the
|
|
197
|
+
neighbouring town is in the same country, balanced and recorded per pair.
|
|
198
|
+
- **A generator model writes the frames** from 3 to 5 example pairs, entity-free, so
|
|
199
|
+
it never sees the contrasted towns. It is unseeded; to rebuild the same pairs,
|
|
200
|
+
reuse their frames: `generate ... --frames-from pairs.jsonl`.
|
|
201
|
+
- **`behaviour`** asks the subject model both sides and scores each clause: is the
|
|
202
|
+
known clause still answered and the unknown one still not, inside the composite?
|
|
203
|
+
It needs `--graded`, a subject model and a template; where any is missing it is
|
|
204
|
+
*skipped*, which is not a pass: `--require behaviour` fails the run on a skip.
|
|
205
|
+
|
|
206
|
+
### 04 · intent pairs: hand-written pairs, your own check, a blind judge
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
cd examples/04-intent-pairs
|
|
210
|
+
contrastset check pairs.jsonl --spec spec.yaml --checks length,overlap,slot_diff,intent_checks.no_intent_words > check.jsonl
|
|
211
|
+
contrastset check pairs.jsonl --spec spec.yaml --checks blind_judge > check-judge.jsonl # calls Haiku
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
No `generate`: the pairs are written by hand (the four in `pairs.jsonl` are stubs),
|
|
215
|
+
and the spec has no slots, only a contrast and checks. `check` needs only `a` and
|
|
216
|
+
`b`; other fields (`scenario`) pass through.
|
|
217
|
+
|
|
218
|
+
- **Your own check** is any function `check(pair, spec, /, **options) -> Verdict |
|
|
219
|
+
None`, referenced by dotted path (`intent_checks.no_intent_words`, a module next to
|
|
220
|
+
the spec). This one fails pairs that carry the contrast in a marker word
|
|
221
|
+
("deliberately"), which `wall-04` does on purpose.
|
|
222
|
+
- **Thresholds are per contrast**: these pairs differ in a clause, so the spec loosens
|
|
223
|
+
`overlap` and `slot_diff`.
|
|
224
|
+
- **`blind_judge`** shows both sides to a judge model in random order and asks which
|
|
225
|
+
one is the target. Over the set it tests the judge's accuracy against chance. For
|
|
226
|
+
a contrast the subject's knowledge defines (example 03) a judge has only surface
|
|
227
|
+
form to go on, so beating chance means a leak. For a contrast readable from meaning,
|
|
228
|
+
like intent, a judge *should* succeed: there it is a manipulation check, not a
|
|
229
|
+
leak test.
|
|
230
|
+
|
|
231
|
+
## The JSONL contract
|
|
232
|
+
|
|
233
|
+
Every JSONL input (entities, a graded file, pairs) is a path or stdin (`-`); a spec
|
|
234
|
+
is a YAML or JSON file. Every verb writes JSONL to stdout (or `-o FILE`), one line per item as soon as it is done, so you can watch partial
|
|
235
|
+
results and stop early (`grade` and `generate` take `--limit N` for a cheap first
|
|
236
|
+
look). What each
|
|
237
|
+
command **requires** from its input; every other field is passed through unchanged
|
|
238
|
+
and listed once on stderr, so a typo in an optional field is visible without
|
|
239
|
+
breaking files written by other tools:
|
|
240
|
+
|
|
241
|
+
| input | required | used if present |
|
|
242
|
+
|---|---|---|
|
|
243
|
+
| entity (`grade`) | `entity`, `type`, `questions[].q`, `questions[].answers` | `questions[].id`, `meta` |
|
|
244
|
+
| graded (`generate --graded`) | `entity`, `questions[].id`, `questions[].bin` | `type`, `meta`, `bin`, `questions[].{q, answers, correct_rate, ...}` |
|
|
245
|
+
| pair (`check`) | `a`, `b` | `pair_id` (else sha256 of a and b), `frame`, `fills`, `factors` |
|
|
246
|
+
|
|
247
|
+
One known gap in v0.1.0: `grade` keeps unknown fields on an entity line but not
|
|
248
|
+
inside its `questions[]` entries (the test for it is marked as an expected failure).
|
|
249
|
+
|
|
250
|
+
Specs are hand-written configuration and stay strict: an unknown key is an error.
|
|
251
|
+
The schemas are in `src/contrastset/records.py` (`GradedView`, `PairView` are the
|
|
252
|
+
read contracts). Every output line carries provenance: model, template hash,
|
|
253
|
+
sampling settings (including the server's defaults), seed, input hashes, time.
|
|
254
|
+
|
|
255
|
+
## Log-prob grading (`grade --logprob`)
|
|
256
|
+
|
|
257
|
+
Sampling 8 answers measures what the model *says*, with sampling noise: a question
|
|
258
|
+
it answers correctly 73 % of the time comes out 8/8 about once in twelve tries.
|
|
259
|
+
With `--adapter hf`, `--logprob` also reads the untempered probabilities: the mass
|
|
260
|
+
of completions that start with a correct answer, with the greedy answer, and with a
|
|
261
|
+
refusal phrase. These are binned with the same thresholds and stored beside the
|
|
262
|
+
sampled numbers (`questions[].logprob`; `logprob_sampler` holds the same under the
|
|
263
|
+
sampling distribution). With `-n 0` the grade uses the probabilities alone: one
|
|
264
|
+
greedy decode plus teacher-forced scoring, no samples, deterministic.
|
|
265
|
+
|
|
266
|
+
**Marginalised over tokenizations.** The probability of a text is the sum over
|
|
267
|
+
every token sequence that spells it, and models often write rare names in a
|
|
268
|
+
segmentation the tokenizer would not choose. Scoring the canonical segmentation
|
|
269
|
+
alone undercounted "Rhineland-Palatinate" on Qwen3-8B by 26× (0.031 vs 0.823).
|
|
270
|
+
contrastset expands, step by step, every token consistent with the rest of the
|
|
271
|
+
answer (a byte trie over the vocabulary, no top-k) and sums the finished paths.
|
|
272
|
+
|
|
273
|
+
**The pruned-mass bound.** The one approximation: a partial path whose probability
|
|
274
|
+
falls below 1e-5 is dropped, and the dropped mass is recorded per question
|
|
275
|
+
(`p_correct_pruned_max`, `p_modal_pruned_max`). The true value lies in
|
|
276
|
+
[p, p + pruned_max]. Pruning limits and the bound go into provenance. Refusal
|
|
277
|
+
phrases are still scored in their canonical segmentation (everyday English, where
|
|
278
|
+
that is the model's own), a known limitation.
|
|
279
|
+
|
|
280
|
+
## One template for grading and probing
|
|
281
|
+
|
|
282
|
+
The template a model is probed or checked with must be the exact string it was
|
|
283
|
+
graded with: bins graded under one wording don't transfer to activations or answers
|
|
284
|
+
collected under another (see Lessons: one line in the prompt can create a whole
|
|
285
|
+
bin). Every graded line records the template's sha256. `check --checks behaviour`
|
|
286
|
+
compares the template it is given with the one in the graded file's provenance and
|
|
287
|
+
refuses a mismatch; `--allow-template-mismatch` overrides, and the override is
|
|
288
|
+
recorded in the check's provenance. Probe code downstream should do the same
|
|
289
|
+
(`grade.require_same_template`).
|
|
290
|
+
|
|
291
|
+
## Backends
|
|
292
|
+
|
|
293
|
+
| adapter | reaches | use |
|
|
294
|
+
|---|---|---|
|
|
295
|
+
| `ollama` (default) | Ollama's native API in raw mode: your template is sent as-is | subject or generator |
|
|
296
|
+
| `openai`, `openai-chat` | any OpenAI-compatible server (vLLM, llama.cpp server, online APIs) | subject or generator |
|
|
297
|
+
| `anthropic` | the Anthropic API (`[anthropic]` extra) | generator only |
|
|
298
|
+
| `hf` | transformers in-process (`[hf]` extra): samples, log-probabilities, activations | subject |
|
|
299
|
+
| `fake` | canned answers, for tests | tests |
|
|
300
|
+
|
|
301
|
+
**The rule: Ollama when samples suffice, HF log-prob for anything that feeds a
|
|
302
|
+
probe.** Ollama is much faster for sample-level questions (does the model answer
|
|
303
|
+
this? does a composite prompt still work?). A graded file that defines the cells of
|
|
304
|
+
a probe should come from the HF adapter with `--logprob`, on the same weights the
|
|
305
|
+
probe loads: a Q4 Ollama model and the bf16 HF model are different models, and
|
|
306
|
+
log-prob masses carry no sampling noise.
|
|
307
|
+
|
|
308
|
+
**Sampling order.** Where contrastset samples itself (the `hf` adapter), it uses one
|
|
309
|
+
order: top-k, then top-p, both on the untempered probabilities, then temperature,
|
|
310
|
+
then the draw. That is llama.cpp's order, so HF and Ollama grades sample alike;
|
|
311
|
+
transformers' own `generate()` tempers first, and then top-p cuts a sharpened
|
|
312
|
+
distribution and the samples come out less varied. Where a server samples
|
|
313
|
+
(`ollama`), provenance records the server's settings, its overrides and its
|
|
314
|
+
version. Every graded line records all sampler parameters, and `--top-k`/`--top-p`
|
|
315
|
+
set them explicitly.
|
|
316
|
+
|
|
317
|
+
## Testing
|
|
318
|
+
|
|
319
|
+
| tier | command | needs | time | when |
|
|
320
|
+
|---|---|---|---|---|
|
|
321
|
+
| fast (default) | `uv run pytest` | nothing: no model, no network | ~10 s | always; the only tier CI runs |
|
|
322
|
+
| smoke | `uv run --extra hf pytest -m smoke` | `[hf]`, Qwen/Qwen3-8B weights, network for the statement sets | ~10 min (M-series Mac) | before a release, after touching anything a probe depends on |
|
|
323
|
+
| regrade | `uv run --extra hf pytest -m regrade` | `[hf]`, Qwen/Qwen3-8B weights | ~27 min (M-series Mac) | when the grading path changes |
|
|
324
|
+
|
|
325
|
+
- **fast** reruns every example in the ladder against its committed files (rescoring
|
|
326
|
+
stored samples, regenerating pairs from stored frames, rechecking), plus unit
|
|
327
|
+
tests with a fake adapter.
|
|
328
|
+
- **smoke** runs example 00 end to end: fetch the statement sets, `check` the
|
|
329
|
+
`cities` pairs (every pair must differ in the country slot only), read activations
|
|
330
|
+
at every layer, fit the per-layer arrow on the frozen split, and assert held-out
|
|
331
|
+
AUC ≥ 0.95 at every layer from 20 to the last.
|
|
332
|
+
- **regrade** regrades the 29 towns of example 03 with HF Qwen3-8B, template B
|
|
333
|
+
(`experiments/templates/qwen3-name-only.txt`), `-n 0 --logprob`, and compares
|
|
334
|
+
with the frozen `tests/regrade/graded-29-B.jsonl`: bins and modal answers must
|
|
335
|
+
match exactly, masses within the pruned-mass bound.
|
|
336
|
+
|
|
337
|
+
## Lessons
|
|
338
|
+
|
|
339
|
+
Two things building this tool taught, with the data in [FINDINGS.md](FINDINGS.md):
|
|
340
|
+
|
|
341
|
+
- **Tokenization undercount.** Log-prob grading over the tokenizer's own
|
|
342
|
+
segmentation undercounts multi-token answers by up to 26×, because the model
|
|
343
|
+
writes rare names in other segmentations. Sum over all of them (above).
|
|
344
|
+
- **The refusal instruction makes the refused bin.** Of 49 towns an earlier grade
|
|
345
|
+
had refused, Qwen3-8B still refused the river for 45 with "If you do not know, say
|
|
346
|
+
'I don't know'" in the template, and for none without it. The refused bin was the instruction working, not a
|
|
347
|
+
state of the model, and the instruction also hid answers the model gives reliably
|
|
348
|
+
without it (3 of the 45). Grade with the template you probe with, and be wary of
|
|
349
|
+
bins a single prompt line can create.
|
|
350
|
+
|
|
351
|
+
The downstream experiment (difference-of-means probes on Qwen3-8B, where example 00
|
|
352
|
+
comes from) is kept in a separate repository, knowing-probe, which pins this
|
|
353
|
+
version.
|
|
354
|
+
|
|
355
|
+
## Not in scope
|
|
356
|
+
|
|
357
|
+
No probing, plotting or model internals in the package: output is a dataset file;
|
|
358
|
+
probe with TransformerLens, nnsight or whatever you already use. (Example 00 has a
|
|
359
|
+
small probe script, outside the package, as a check on that stage.) The design
|
|
360
|
+
brief is [PLAN.md](PLAN.md); maintenance passes are logged in
|
|
361
|
+
[NOTEBOOK.md](NOTEBOOK.md).
|
|
362
|
+
|
|
363
|
+
## Licence
|
|
364
|
+
|
|
365
|
+
MIT, see [LICENSE](LICENSE).
|