brainpatch 1.2.0__py3-none-any.whl

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.
Files changed (82) hide show
  1. brainpatch/__init__.py +92 -0
  2. brainpatch/backends/__init__.py +19 -0
  3. brainpatch/backends/llamacpp.py +383 -0
  4. brainpatch/backends/mlx_backend.py +213 -0
  5. brainpatch/backends/transformers_backend.py +473 -0
  6. brainpatch/backends/vllm_backend.py +299 -0
  7. brainpatch/backends/vllm_worker.py +129 -0
  8. brainpatch/cli.py +825 -0
  9. brainpatch/config.py +245 -0
  10. brainpatch/datasets/__init__.py +20 -0
  11. brainpatch/datasets/contrast_sets.py +64 -0
  12. brainpatch/evaluation/__init__.py +28 -0
  13. brainpatch/evaluation/metrics.py +223 -0
  14. brainpatch/patch/__init__.py +64 -0
  15. brainpatch/patch/compiler.py +324 -0
  16. brainpatch/patch/format.py +489 -0
  17. brainpatch/patch/loader.py +312 -0
  18. brainpatch/patch/registry.py +300 -0
  19. brainpatch/patch/tensors.py +236 -0
  20. brainpatch/patch/validation.py +157 -0
  21. brainpatch/paths.py +184 -0
  22. brainpatch/py.typed +0 -0
  23. brainpatch/research/__init__.py +16 -0
  24. brainpatch/research/antisycophancy.py +348 -0
  25. brainpatch/research/behaviour_eval.py +711 -0
  26. brainpatch/research/generation_eval.py +346 -0
  27. brainpatch/research/ml/__init__.py +35 -0
  28. brainpatch/research/ml/activation_store.py +232 -0
  29. brainpatch/research/ml/causal.py +386 -0
  30. brainpatch/research/ml/corpus.py +165 -0
  31. brainpatch/research/ml/evaluation.py +188 -0
  32. brainpatch/research/ml/extraction.py +464 -0
  33. brainpatch/research/ml/feature_analysis.py +317 -0
  34. brainpatch/research/ml/generation.py +109 -0
  35. brainpatch/research/ml/hooks.py +183 -0
  36. brainpatch/research/ml/intervention.py +274 -0
  37. brainpatch/research/ml/model.py +219 -0
  38. brainpatch/research/ml/patch_search.py +337 -0
  39. brainpatch/research/ml/runtime.py +343 -0
  40. brainpatch/research/ml/sae.py +383 -0
  41. brainpatch/research/ml/training.py +376 -0
  42. brainpatch/research/stance_rubric.py +170 -0
  43. brainpatch/research/sycophancy_data.py +982 -0
  44. brainpatch/research/sycophancy_data_r1.py +1701 -0
  45. brainpatch/research/sycophancy_data_v2.py +1649 -0
  46. brainpatch/research/sycophancy_data_v3.py +2288 -0
  47. brainpatch/research/sycophancy_v2_build.py +362 -0
  48. brainpatch/research/sycophancy_v3_build.py +188 -0
  49. brainpatch/research/utility_probe.py +139 -0
  50. brainpatch/runtime/__init__.py +50 -0
  51. brainpatch/runtime/auto.py +157 -0
  52. brainpatch/runtime/base.py +311 -0
  53. brainpatch/runtime/capabilities.py +96 -0
  54. brainpatch/runtime/model.py +260 -0
  55. brainpatch/runtime/scheduling.py +13 -0
  56. brainpatch/schemas/__init__.py +35 -0
  57. brainpatch/schemas/contrast.py +161 -0
  58. brainpatch/schemas/feature.py +193 -0
  59. brainpatch/schemas/manifest.py +167 -0
  60. brainpatch/schemas/patch.py +379 -0
  61. brainpatch/schemas/patch_io.py +88 -0
  62. brainpatch/schemas/sae.py +146 -0
  63. brainpatch/server/__init__.py +11 -0
  64. brainpatch/server/app.py +269 -0
  65. brainpatch/steering/__init__.py +13 -0
  66. brainpatch/steering/plan.py +177 -0
  67. brainpatch/steering/schedule.py +138 -0
  68. brainpatch/ui/__init__.py +11 -0
  69. brainpatch/ui/app.py +201 -0
  70. brainpatch/verify/__init__.py +66 -0
  71. brainpatch/verify/behavioural.py +156 -0
  72. brainpatch/verify/checks.py +204 -0
  73. brainpatch/verify/corruptions.py +335 -0
  74. brainpatch/verify/report.py +133 -0
  75. brainpatch/verify/vectors.py +95 -0
  76. brainpatch/verify/workflow.py +331 -0
  77. brainpatch-1.2.0.dist-info/METADATA +556 -0
  78. brainpatch-1.2.0.dist-info/RECORD +82 -0
  79. brainpatch-1.2.0.dist-info/WHEEL +5 -0
  80. brainpatch-1.2.0.dist-info/entry_points.txt +2 -0
  81. brainpatch-1.2.0.dist-info/licenses/LICENSE +190 -0
  82. brainpatch-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,95 @@
1
+ """Pure-Python vector helpers for corruption generation and static validation.
2
+
3
+ No torch. A corruption generator transforms an *inert data file* — a manifest and
4
+ a list of floats — and has no business requiring the ML stack. Keeping it
5
+ dependency-free is what lets the corruption suite and every static detection layer
6
+ run in the fast local test suite, where `tests/conftest.py` blocks torch on
7
+ purpose.
8
+
9
+ Generation and execution tracing genuinely need a model; those live in
10
+ `brainpatch.verify.behavioural` and stay Modal-only.
11
+
12
+ Arithmetic is done in Python floats (IEEE double), which is *more* precise than
13
+ the float32 path it replaces — an earlier float32 dot product over 1536 dimensions
14
+ reported cosine 0.99999994 for a bit-identical vector.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import math
20
+ import random
21
+ import struct
22
+ from collections.abc import Sequence
23
+
24
+
25
+ def dot(a: Sequence[float], b: Sequence[float]) -> float:
26
+ return math.fsum(x * y for x, y in zip(a, b))
27
+
28
+
29
+ def norm(a: Sequence[float]) -> float:
30
+ return math.sqrt(math.fsum(x * x for x in a))
31
+
32
+
33
+ def scaled(a: Sequence[float], factor: float) -> list[float]:
34
+ return [x * factor for x in a]
35
+
36
+
37
+ def unit(a: Sequence[float]) -> list[float]:
38
+ n = norm(a)
39
+ if n == 0.0:
40
+ raise ValueError("cannot normalise a zero vector")
41
+ return [x / n for x in a]
42
+
43
+
44
+ def cosine(a: Sequence[float], b: Sequence[float]) -> float:
45
+ denominator = norm(a) * norm(b)
46
+ if denominator == 0.0:
47
+ raise ValueError("cosine of a zero vector is undefined")
48
+ return dot(a, b) / denominator
49
+
50
+
51
+ def gaussian(size: int, *, seed: int) -> list[float]:
52
+ """Deterministic standard normals.
53
+
54
+ `random.Random(seed).gauss` is reproducible across platforms and Python
55
+ versions, which is what a recorded corruption needs.
56
+ """
57
+ rng = random.Random(seed)
58
+ return [rng.gauss(0.0, 1.0) for _ in range(size)]
59
+
60
+
61
+ def orthogonalise(candidate: Sequence[float], against: Sequence[float]) -> list[float]:
62
+ """Remove the component of `candidate` parallel to `against`."""
63
+ direction = unit(against)
64
+ projection = dot(candidate, direction)
65
+ return [c - projection * d for c, d in zip(candidate, direction)]
66
+
67
+
68
+ def to_float16(value: float) -> float:
69
+ """Round a Python float to IEEE half precision and back.
70
+
71
+ `struct` format `'e'` is binary16 with round-to-nearest-even, matching what a
72
+ float16 store does.
73
+ """
74
+ return struct.unpack("<e", struct.pack("<e", value))[0]
75
+
76
+
77
+ def to_bfloat16(value: float) -> float:
78
+ """Round to bfloat16 and back, round-to-nearest-even.
79
+
80
+ bfloat16 keeps float32's exponent and truncates the mantissa to 7 bits, so it
81
+ is coarser than float16 in precision while covering a far wider range. Done by
82
+ hand because `struct` has no bf16 code; the rounding bias below is the
83
+ standard RNE construction and matches `torch.Tensor.to(torch.bfloat16)`.
84
+ """
85
+ bits = struct.unpack("<I", struct.pack("<f", value))[0]
86
+ if (bits & 0x7F800000) == 0x7F800000: # inf or NaN: pass through untouched
87
+ return value
88
+ bias = 0x7FFF + ((bits >> 16) & 1)
89
+ rounded = (bits + bias) & 0xFFFF0000
90
+ return struct.unpack("<f", struct.pack("<I", rounded))[0]
91
+
92
+
93
+ def round_to(values: Sequence[float], dtype: str) -> list[float]:
94
+ convert = {"float16": to_float16, "bfloat16": to_bfloat16}[dtype]
95
+ return [convert(v) for v in values]
@@ -0,0 +1,331 @@
1
+ """The `brainpatch verify` workflow.
2
+
3
+ One entry point that runs every check the available inputs allow, and is explicit
4
+ about which ones it could not run. A check that was skipped is reported as
5
+ `skipped`, never silently omitted -- an absent check reads as a pass, and that is
6
+ how a bad artifact gets trusted.
7
+
8
+ The levels degrade gracefully:
9
+
10
+ * no reference, no model -> structural only
11
+ * reference, no model -> structural + numerical
12
+ * model, no reference -> structural + execution
13
+ * both -> structural + numerical + execution
14
+ * plus recorded behaviour -> all four
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ from brainpatch.verify import vectors as vec
24
+ from brainpatch.verify.checks import (
25
+ COSINE_TOLERANCE,
26
+ NORM_TOLERANCE,
27
+ Reference,
28
+ reference_from,
29
+ run_execution_trace,
30
+ run_static_layers,
31
+ )
32
+
33
+ #: Signed cosine below this fails numerical verification. A cosine against an
34
+ #: *unsigned* reference is never used: it cannot distinguish v from -v.
35
+ MIN_SIGNED_COSINE = 1.0 - COSINE_TOLERANCE
36
+
37
+ LEVELS = ("structural", "numerical", "execution", "behavioural")
38
+
39
+ PASS = "pass"
40
+ FAIL = "fail"
41
+ SKIPPED = "skipped"
42
+
43
+
44
+ @dataclass
45
+ class Check:
46
+ """One named check and what it found."""
47
+
48
+ name: str
49
+ level: str
50
+ status: str
51
+ detail: str = ""
52
+ measured: dict[str, Any] = field(default_factory=dict)
53
+
54
+ @property
55
+ def ok(self) -> bool:
56
+ return self.status != FAIL
57
+
58
+
59
+ @dataclass
60
+ class VerificationResult:
61
+ """The outcome of `verify_artifact`.
62
+
63
+ ``faithful`` is True only when nothing failed. Skipped checks do not fail a
64
+ verification, but they do mean the artifact is unverified in that respect,
65
+ which :attr:`skipped_levels` makes explicit so a caller cannot mistake
66
+ "not checked" for "checked and fine".
67
+ """
68
+
69
+ artifact: str
70
+ sha256: str
71
+ checks: list[Check]
72
+ levels: dict[str, str]
73
+
74
+ @property
75
+ def failures(self) -> list[Check]:
76
+ return [c for c in self.checks if c.status == FAIL]
77
+
78
+ @property
79
+ def skipped_levels(self) -> list[str]:
80
+ return [name for name, state in self.levels.items() if state == SKIPPED]
81
+
82
+ @property
83
+ def faithful(self) -> bool:
84
+ return not self.failures
85
+
86
+ @property
87
+ def verdict(self) -> str:
88
+ if self.failures:
89
+ behavioural_only = all(c.level == "behavioural" for c in self.failures)
90
+ return "FAILED_BEHAVIORAL_FIDELITY" if behavioural_only else "UNFAITHFUL"
91
+ return "FAITHFUL"
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ return {
95
+ "artifact": self.artifact,
96
+ "sha256": self.sha256,
97
+ "verdict": self.verdict,
98
+ "faithful": self.faithful,
99
+ "levels": self.levels,
100
+ "skipped_levels": self.skipped_levels,
101
+ "checks": [
102
+ {
103
+ "name": c.name,
104
+ "level": c.level,
105
+ "status": c.status,
106
+ "detail": c.detail,
107
+ "measured": c.measured,
108
+ }
109
+ for c in self.checks
110
+ ],
111
+ }
112
+
113
+
114
+ def _digest(path: Path) -> str:
115
+ import hashlib
116
+
117
+ return hashlib.sha256(path.read_bytes()).hexdigest()
118
+
119
+
120
+ def verify_artifact(
121
+ path: str | Path,
122
+ *,
123
+ reference: str | Path | Reference | None = None,
124
+ backend: Any = None,
125
+ expect_model: str | None = None,
126
+ ) -> VerificationResult:
127
+ """Verify one artifact as far as the given inputs allow.
128
+
129
+ Parameters
130
+ ----------
131
+ path:
132
+ The `.brainpatch` archive under test.
133
+ reference:
134
+ A trusted artifact, or a :class:`Reference`, to compare direction and
135
+ magnitude against. Without it, numerical verification is skipped -- there
136
+ is nothing to be right or wrong relative to.
137
+ backend:
138
+ A loaded backend. Without it, execution verification is skipped and the
139
+ artifact's declared layer and site remain unconfirmed against what the
140
+ runtime would actually do.
141
+ expect_model:
142
+ Model id the caller intends to load the patch into. Compared against the
143
+ manifest's declared base model.
144
+ """
145
+ from brainpatch.patch.format import PatchFormatError
146
+ from brainpatch.patch.loader import PatchLoadError, load_patch
147
+
148
+ artifact = Path(path)
149
+ checks: list[Check] = []
150
+ levels = {level: SKIPPED for level in LEVELS}
151
+
152
+ # ---- structural ---------------------------------------------------------
153
+ try:
154
+ loaded = load_patch(artifact, verify_checksums=True)
155
+ checks.append(Check("checksums", "structural", PASS, "archive checksums verify"))
156
+ except PatchLoadError as exc:
157
+ checks.append(Check("checksums", "structural", FAIL, str(exc)))
158
+ levels["structural"] = FAIL
159
+ return VerificationResult(str(artifact), _digest(artifact), checks, levels)
160
+
161
+ try:
162
+ loaded.manifest.validate()
163
+ checks.append(Check("schema", "structural", PASS, "manifest is self-consistent"))
164
+ except PatchFormatError as exc:
165
+ checks.append(Check("schema", "structural", FAIL, str(exc)))
166
+
167
+ manifest = loaded.manifest
168
+ intervention = manifest.interventions[0]
169
+ tensor = loaded.vectors[intervention.vector]
170
+ hidden = manifest.base_model.hidden_size
171
+ shape_ok = list(tensor.shape) == [hidden]
172
+ checks.append(
173
+ Check(
174
+ "shape_dtype", "structural", PASS if shape_ok else FAIL,
175
+ f"vector {intervention.vector!r} shape {list(tensor.shape)}, "
176
+ f"dtype {tensor.dtype}, model hidden size {hidden}",
177
+ {"shape": list(tensor.shape), "dtype": tensor.dtype},
178
+ )
179
+ )
180
+
181
+ if expect_model is not None:
182
+ declared = manifest.base_model.model_id
183
+ match = declared == expect_model
184
+ checks.append(
185
+ Check(
186
+ "model_compatibility", "structural", PASS if match else FAIL,
187
+ f"declares {declared!r}, target is {expect_model!r}",
188
+ {"declared": declared, "expected": expect_model},
189
+ )
190
+ )
191
+ else:
192
+ checks.append(
193
+ Check(
194
+ "model_compatibility", "structural", SKIPPED,
195
+ f"no target model given; artifact declares "
196
+ f"{manifest.base_model.model_id!r}",
197
+ )
198
+ )
199
+
200
+ levels["structural"] = (
201
+ FAIL if any(c.level == "structural" and c.status == FAIL for c in checks) else PASS
202
+ )
203
+
204
+ # ---- numerical ----------------------------------------------------------
205
+ if reference is None:
206
+ checks.append(
207
+ Check(
208
+ "signed_direction", "numerical", SKIPPED,
209
+ "no reference artifact given; direction and magnitude are unverified",
210
+ )
211
+ )
212
+ else:
213
+ ref = reference if isinstance(reference, Reference) else reference_from(Path(reference))
214
+ delta = vec.scaled([float(v) for v in tensor.data], intervention.coefficient)
215
+ cosine = vec.cosine(delta, ref.delta)
216
+ ratio = vec.norm(delta) / vec.norm(ref.delta)
217
+
218
+ direction_ok = cosine >= MIN_SIGNED_COSINE
219
+ checks.append(
220
+ Check(
221
+ "signed_direction", "numerical", PASS if direction_ok else FAIL,
222
+ f"signed cosine {cosine:+.8f} (threshold {MIN_SIGNED_COSINE:.6f}); "
223
+ f"unsigned |cos| {abs(cosine):.8f} would have "
224
+ f"{'passed' if abs(cosine) >= MIN_SIGNED_COSINE else 'failed'}",
225
+ {"signed_cosine": cosine, "unsigned_cosine": abs(cosine)},
226
+ )
227
+ )
228
+ # Direction and magnitude together fully determine the applied delta, so
229
+ # the coefficient is reported rather than checked separately: a wrong
230
+ # coefficient shows up here as a norm ratio, or as a negative cosine if
231
+ # its sign flipped.
232
+ norm_ok = abs(ratio - 1.0) <= NORM_TOLERANCE
233
+ checks.append(
234
+ Check(
235
+ "delta_norm", "numerical", PASS if norm_ok else FAIL,
236
+ f"applied |delta| {vec.norm(delta):.5f} against reference "
237
+ f"{vec.norm(ref.delta):.5f} (ratio {ratio:.6f}), "
238
+ f"coefficient {intervention.coefficient:+.6f}",
239
+ {
240
+ "delta_norm": vec.norm(delta),
241
+ "norm_ratio": ratio,
242
+ "coefficient": intervention.coefficient,
243
+ },
244
+ )
245
+ )
246
+ levels["numerical"] = (
247
+ FAIL if any(c.level == "numerical" and c.status == FAIL for c in checks) else PASS
248
+ )
249
+
250
+ # ---- execution ----------------------------------------------------------
251
+ if backend is None or getattr(backend, "model", None) is None:
252
+ checks.append(
253
+ Check(
254
+ "execution_trace", "execution", SKIPPED,
255
+ "no loaded backend given; declared layer and site are unconfirmed "
256
+ "against what the runtime would do",
257
+ )
258
+ )
259
+ else:
260
+ declared = Reference(
261
+ delta=[],
262
+ layer=int(intervention.layer),
263
+ site=str(intervention.site),
264
+ model_id=manifest.base_model.model_id,
265
+ revision=manifest.base_model.revision,
266
+ shape=list(tensor.shape),
267
+ schedule=manifest.schedule,
268
+ )
269
+ num_layers = len(backend._decoder_layers())
270
+ trace = run_execution_trace(backend, artifact, declared, num_layers)
271
+ # `caught` here means "differs from what the manifest declares", which is
272
+ # a verification failure rather than a detection success.
273
+ checks.append(
274
+ Check(
275
+ "execution_trace", "execution", FAIL if trace["caught"] else PASS,
276
+ f"runtime edits on prompt pass at layers {trace['prompt_layers']}, "
277
+ f"on continuation pass at {trace['continuation_layers']}; "
278
+ f"manifest declares layer {declared.layer} site {declared.site!r}",
279
+ trace,
280
+ )
281
+ )
282
+ schedule_ok = manifest.schedule == declared.schedule
283
+ checks.append(
284
+ Check(
285
+ "schedule", "execution", PASS if schedule_ok else FAIL,
286
+ f"schedule {manifest.schedule!r}",
287
+ {"schedule": manifest.schedule},
288
+ )
289
+ )
290
+ levels["execution"] = (
291
+ FAIL if any(c.level == "execution" and c.status == FAIL for c in checks) else PASS
292
+ )
293
+
294
+ checks.append(
295
+ Check(
296
+ "behavioural_regression", "behavioural", SKIPPED,
297
+ "run `verify_behavioural_regression` with a recorded result and a "
298
+ "frozen evaluation set to check this level",
299
+ )
300
+ )
301
+
302
+ return VerificationResult(str(artifact), _digest(artifact), checks, levels)
303
+
304
+
305
+ def verify_behavioural_regression(
306
+ measured_effect: float,
307
+ recorded_effect: float,
308
+ *,
309
+ tolerance: float,
310
+ ) -> Check:
311
+ """Compare a freshly measured effect against a recorded one.
312
+
313
+ ``tolerance`` must be chosen before measuring. Note the empirical caveat from
314
+ ``experiments/artifact_fidelity_v1``: at a tolerance calibrated to path noise,
315
+ this check was *less* sensitive to magnitude errors than a delta-norm
316
+ comparison, so it complements the numerical level rather than replacing it.
317
+ """
318
+ delta = abs(measured_effect - recorded_effect)
319
+ return Check(
320
+ "behavioural_regression",
321
+ "behavioural",
322
+ PASS if delta <= tolerance else FAIL,
323
+ f"effect {measured_effect:+.4f} against recorded {recorded_effect:+.4f} "
324
+ f"(delta {delta:.4f}, tolerance {tolerance})",
325
+ {
326
+ "measured_effect": measured_effect,
327
+ "recorded_effect": recorded_effect,
328
+ "delta": delta,
329
+ "tolerance": tolerance,
330
+ },
331
+ )