mechbench-compute 0.10.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.
- mechbench_compute-0.10.0/PKG-INFO +179 -0
- mechbench_compute-0.10.0/README.md +147 -0
- mechbench_compute-0.10.0/mechbench_compute/__init__.py +312 -0
- mechbench_compute-0.10.0/mechbench_compute/_arch.py +267 -0
- mechbench_compute-0.10.0/mechbench_compute/_forward.py +358 -0
- mechbench_compute-0.10.0/mechbench_compute/_forward_gemma3.py +218 -0
- mechbench_compute-0.10.0/mechbench_compute/_forward_llama.py +205 -0
- mechbench_compute-0.10.0/mechbench_compute/_forward_qwen.py +192 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke.py +74 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke_12b.py +129 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke_attribution.py +99 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke_bench.py +77 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke_bitexact.py +171 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke_distill.py +332 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke_e2b.py +152 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke_interventions.py +67 -0
- mechbench_compute-0.10.0/mechbench_compute/_smoke_plots.py +174 -0
- mechbench_compute-0.10.0/mechbench_compute/attribution.py +254 -0
- mechbench_compute-0.10.0/mechbench_compute/backends.py +91 -0
- mechbench_compute-0.10.0/mechbench_compute/bench.py +229 -0
- mechbench_compute-0.10.0/mechbench_compute/blocks.py +486 -0
- mechbench_compute-0.10.0/mechbench_compute/cache.py +81 -0
- mechbench_compute-0.10.0/mechbench_compute/distill.py +753 -0
- mechbench_compute-0.10.0/mechbench_compute/errors.py +81 -0
- mechbench_compute-0.10.0/mechbench_compute/finetune.py +113 -0
- mechbench_compute-0.10.0/mechbench_compute/generate.py +295 -0
- mechbench_compute-0.10.0/mechbench_compute/geometry.py +614 -0
- mechbench_compute-0.10.0/mechbench_compute/head_weights.py +478 -0
- mechbench_compute-0.10.0/mechbench_compute/hooks.py +106 -0
- mechbench_compute-0.10.0/mechbench_compute/hub.py +186 -0
- mechbench_compute-0.10.0/mechbench_compute/interventions.py +578 -0
- mechbench_compute-0.10.0/mechbench_compute/inventory.py +185 -0
- mechbench_compute-0.10.0/mechbench_compute/lens.py +124 -0
- mechbench_compute-0.10.0/mechbench_compute/lm_bridge.py +77 -0
- mechbench_compute-0.10.0/mechbench_compute/lora.py +137 -0
- mechbench_compute-0.10.0/mechbench_compute/model.py +439 -0
- mechbench_compute-0.10.0/mechbench_compute/peft.py +123 -0
- mechbench_compute-0.10.0/mechbench_compute/platform_kinds.py +138 -0
- mechbench_compute-0.10.0/mechbench_compute/plot.py +817 -0
- mechbench_compute-0.10.0/mechbench_compute/probes.py +274 -0
- mechbench_compute-0.10.0/mechbench_compute/prompts.py +212 -0
- mechbench_compute-0.10.0/mechbench_compute/protocol.py +1198 -0
- mechbench_compute-0.10.0/mechbench_compute.egg-info/PKG-INFO +179 -0
- mechbench_compute-0.10.0/mechbench_compute.egg-info/SOURCES.txt +51 -0
- mechbench_compute-0.10.0/mechbench_compute.egg-info/dependency_links.txt +1 -0
- mechbench_compute-0.10.0/mechbench_compute.egg-info/requires.txt +18 -0
- mechbench_compute-0.10.0/mechbench_compute.egg-info/top_level.txt +1 -0
- mechbench_compute-0.10.0/pyproject.toml +51 -0
- mechbench_compute-0.10.0/setup.cfg +4 -0
- mechbench_compute-0.10.0/tests/test_backends.py +112 -0
- mechbench_compute-0.10.0/tests/test_blocks.py +173 -0
- mechbench_compute-0.10.0/tests/test_hub.py +16 -0
- mechbench_compute-0.10.0/tests/test_inventory.py +130 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mechbench-compute
|
|
3
|
+
Version: 0.10.0
|
|
4
|
+
Summary: Composable mechanistic-interpretability primitives: hook-aware forward, interventions, activation cache, logit lens, probes, and geometry.
|
|
5
|
+
Author-email: Benji Smith <benji@shaxpir.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mechbench/mechbench-compute
|
|
8
|
+
Project-URL: Website, https://mechbench.ai
|
|
9
|
+
Keywords: interpretability,mechanistic-interpretability,mlx,transformers,gemma
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: huggingface-hub>=0.24
|
|
19
|
+
Requires-Dist: mechbench-schema>=0.13
|
|
20
|
+
Requires-Dist: mlx>=0.20; sys_platform == "darwin" and platform_machine == "arm64"
|
|
21
|
+
Requires-Dist: mlx-lm>=0.20; sys_platform == "darwin" and platform_machine == "arm64"
|
|
22
|
+
Requires-Dist: mlx-vlm>=0.6.1; sys_platform == "darwin" and platform_machine == "arm64"
|
|
23
|
+
Requires-Dist: numpy>=1.26
|
|
24
|
+
Requires-Dist: transformers>=4.40
|
|
25
|
+
Requires-Dist: matplotlib>=3.8
|
|
26
|
+
Requires-Dist: scikit-learn>=1.4
|
|
27
|
+
Provides-Extra: eval
|
|
28
|
+
Requires-Dist: lm-eval>=0.4; extra == "eval"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
32
|
+
|
|
33
|
+
# mechbench-compute
|
|
34
|
+
|
|
35
|
+
The compute engine for [mechbench](https://mechbench.ai) — composable mechanistic-interpretability primitives built on MLX.
|
|
36
|
+
|
|
37
|
+
This repo provides:
|
|
38
|
+
|
|
39
|
+
- **Hook-aware forward pass.** One canonical path through the model; instrumentation via named hook points and TransformerLens-style callbacks.
|
|
40
|
+
- **Declarative interventions.** `Ablate`, `Capture`, `Patch` primitives composable into a single `model.run(..., interventions=[...])` call.
|
|
41
|
+
- **Activation cache.** `ActivationCache` container for collected activations; bf16 throughout, float32 only at the analysis boundary.
|
|
42
|
+
- **Architecture adapter.** `Arch` dataclass that handles per-variant differences (layer count, global-attention pattern, RoPE parameters, etc.). Currently supports Gemma 4 E4B and E2B; the adapter pattern follows TransformerLens 3's `TransformerBridge`.
|
|
43
|
+
- **Analysis helpers.** Logit lens, direct logit attribution (`accumulated_resid`, `decompose_resid`, `head_results`, `logit_attrs`), fact vectors, centroid decoding, probe primitives, head-weight static analysis, geometry metrics.
|
|
44
|
+
- **Plot helpers.** Matplotlib conventions baked in for quick diagnostic figures — not the full visualization surface (that lives in `mechbench-ui`).
|
|
45
|
+
|
|
46
|
+
See [`PACKAGE_README.md`](PACKAGE_README.md) for the full API tour and worked examples.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install mechbench-compute
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
From source:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/mechbench/mechbench-compute.git
|
|
58
|
+
cd mechbench-compute
|
|
59
|
+
pip install -e '.[dev]'
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Apple Silicon required (MLX is the only supported backend today). A PyTorch backend would live as `mechbench_compute.backends.torch` alongside the MLX one if/when the need arises; splitting repos by backend is explicitly not planned.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from mechbench_compute import Model, Ablate, Capture
|
|
68
|
+
|
|
69
|
+
model = Model.load()
|
|
70
|
+
ids = model.tokenize("Complete this sentence with one word: The Eiffel Tower is in")
|
|
71
|
+
|
|
72
|
+
result = model.run(ids)
|
|
73
|
+
for tok, p in result.top_k(model.tokenizer, k=5):
|
|
74
|
+
print(f"{tok!r:20s} p={p:.4f}")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Distributional-target training (`distill` + `lora`)
|
|
78
|
+
|
|
79
|
+
Primitives for training a model toward a specified *distribution* over
|
|
80
|
+
responses rather than toward example responses (task
|
|
81
|
+
`000114`):
|
|
82
|
+
soft-target cross-entropy at decision tokens has gradient P − T, so the
|
|
83
|
+
adapter learns to *emit the distribution*.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
import mlx.nn as nn
|
|
87
|
+
import mlx.optimizers as optim
|
|
88
|
+
import numpy as np
|
|
89
|
+
from mechbench_compute import Model, distill, lora
|
|
90
|
+
from mechbench_compute.distill import TargetMap
|
|
91
|
+
|
|
92
|
+
model = Model.load()
|
|
93
|
+
tok = model.tokenizer
|
|
94
|
+
|
|
95
|
+
# A target is a Map<String, Double> — hardcoded, from JSON, or uniform —
|
|
96
|
+
# with whole-map transforms that each return a new map:
|
|
97
|
+
target = TargetMap.from_json("weights.json").sqrt().normalize()
|
|
98
|
+
target = TargetMap.uniform([str(i) for i in range(1, 7)]) # fair d6
|
|
99
|
+
|
|
100
|
+
# Compile it against the rendered prompt: items become token paths
|
|
101
|
+
# (multi-token items share trie nodes; a closer appends continuation
|
|
102
|
+
# anchors so the flattening can't leak past the envelope):
|
|
103
|
+
prompt = distill.render_chat(tok, system, "Please roll the die.",
|
|
104
|
+
prefill='{ "roll": ')
|
|
105
|
+
trie = target.tokenize(tok, prompt, closer=" }")
|
|
106
|
+
|
|
107
|
+
n = lora.apply_lora(model.lm) # freeze + wrap q/v projections
|
|
108
|
+
step = nn.value_and_grad(model.lm, distill.soft_ce)
|
|
109
|
+
opt = optim.Adam(learning_rate=1e-4)
|
|
110
|
+
rng = np.random.default_rng(7)
|
|
111
|
+
for _ in range(steps):
|
|
112
|
+
batch = [trie.hard_example(trie.sample(rng)) for _ in range(3)]
|
|
113
|
+
batch.append(trie.marginal_example()) # exact first-token marginal
|
|
114
|
+
batch.append(sharp_anchor) # keeps confident tasks sharp
|
|
115
|
+
loss, grads = step(model.lm, batch)
|
|
116
|
+
opt.update(model.lm, grads)
|
|
117
|
+
|
|
118
|
+
lora.save_adapter(model.lm, "adapter.safetensors")
|
|
119
|
+
# Later, on a fresh model: merge + exact undo
|
|
120
|
+
handle = lora.fuse(model.lm, lora.load_adapter("adapter.safetensors"),
|
|
121
|
+
scale=16 / 8) # alpha / rank from training
|
|
122
|
+
lora.restore(model.lm, handle)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Calibration is measured at item level (`trie.score`, `distill.item_metrics`
|
|
126
|
+
— captured mass, entropy, KL from target) and at the decision token
|
|
127
|
+
(`distill.first_token_metrics`). `python -m mechbench_compute._smoke_distill`
|
|
128
|
+
runs the full lifecycle on E2B.
|
|
129
|
+
|
|
130
|
+
**Two forward paths.** Training and scoring call `Model.lm` (the text
|
|
131
|
+
decoder, uniform across families) directly — plain module calls,
|
|
132
|
+
differentiable, no instrumentation. `Model.run` remains the hook-aware
|
|
133
|
+
forward for capture/patch/lens work. Adapters bridge the two: `fuse` an
|
|
134
|
+
adapter into the weights and every instrumented run sees the adapted
|
|
135
|
+
model; `restore` flips it back, so base-vs-adapted comparisons run in one
|
|
136
|
+
script.
|
|
137
|
+
|
|
138
|
+
**Scoring tiers** (task 000227): `score_items` is the sequential
|
|
139
|
+
reference oracle; `score_items_batched` adds length-bucketed batching
|
|
140
|
+
(~1.5×); `score_items_fast` additionally splits the forward via
|
|
141
|
+
`Model.trunk_hidden` / `Model.head_logits` and unembeds only the
|
|
142
|
+
supervised rows (~1.5–2.1× vs oracle, family-dependent — best when
|
|
143
|
+
items share no prefix, e.g. cross-document scoring);
|
|
144
|
+
`score_items_cached` encodes a shared prompt **once** into a KV cache
|
|
145
|
+
(`Model.prompt_cache`) and scores each item's 1–4 suffix tokens against
|
|
146
|
+
a per-item copy — the tier for shared-prompt batteries. Measured
|
|
147
|
+
(flat name batteries, cached vs oracle): E2B 2.8×, Gemma-3-4B 3.4×,
|
|
148
|
+
Qwen-3B 2.6×, Llama-8B 3.0×; positions/attention exact by construction,
|
|
149
|
+
bf16 envelope from decomposed attention: mass-region |ΔlogP| ≤ 0.45,
|
|
150
|
+
renormalized KL ≤ 2.4e-2 bits, idempotent (0.0 across repeat calls).
|
|
151
|
+
Fast-tier envelope: max |ΔlogP| ≤ 0.99 (deep tail) / ≤ 0.24
|
|
152
|
+
(mass region), renorm KL ≤ 6.5e-3 bits. All of it is bf16 matmul
|
|
153
|
+
tiling — same rows, same math, verified bit-exact where shapes match.
|
|
154
|
+
Flat-target KL diagnostics weight the tail, so switch tiers only
|
|
155
|
+
between comparisons, never mid-experiment. Known upstream limit
|
|
156
|
+
(mlx 0.31.2 / mlx-lm 0.31.3 / mlx-vlm 0.6.1): **batched cached decoding
|
|
157
|
+
corrupts every batch row after the first** on both stacks (reproduced
|
|
158
|
+
with natively built B=4 caches and identical rows), which is why the
|
|
159
|
+
cached tier is per-item; batched suffix scoring behind an upstream fix
|
|
160
|
+
is the remaining ~5–10× path.
|
|
161
|
+
|
|
162
|
+
## Status
|
|
163
|
+
|
|
164
|
+
The `Arch` adapter supports Gemma 4 E4B and E2B; generalization to other architecture families is ongoing.
|
|
165
|
+
|
|
166
|
+
The substrate epic that will define how intermediate results are cached and shared across experiments is `000162` (DAG solver + content-addressed memoization). It consumes the canonical-serialization guarantee from `000161` (binary formats) and the path grammar from `000163` (identity scheme).
|
|
167
|
+
|
|
168
|
+
## Relationship to other mechbench repos
|
|
169
|
+
|
|
170
|
+
- **`mechbench-schema`** — the typed emission contract. `mechbench-compute` emits records shaped by schema types.
|
|
171
|
+
- **`mechbench-experiments`** — research scripts and findings that consume this package. Uses `mechbench-compute` as its primary dependency.
|
|
172
|
+
- **`mechbench-runner`** — exposes these primitives as agent-callable tools. Imports `mechbench-compute`.
|
|
173
|
+
- **`mechbench-ui`** — TypeScript frontend. Does not import `mechbench-compute` directly; reads bundles produced by it through the `mechbench-schema` contract.
|
|
174
|
+
|
|
175
|
+
See [mechbench.ai](https://mechbench.ai) for the family overview and the design principles.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# mechbench-compute
|
|
2
|
+
|
|
3
|
+
The compute engine for [mechbench](https://mechbench.ai) — composable mechanistic-interpretability primitives built on MLX.
|
|
4
|
+
|
|
5
|
+
This repo provides:
|
|
6
|
+
|
|
7
|
+
- **Hook-aware forward pass.** One canonical path through the model; instrumentation via named hook points and TransformerLens-style callbacks.
|
|
8
|
+
- **Declarative interventions.** `Ablate`, `Capture`, `Patch` primitives composable into a single `model.run(..., interventions=[...])` call.
|
|
9
|
+
- **Activation cache.** `ActivationCache` container for collected activations; bf16 throughout, float32 only at the analysis boundary.
|
|
10
|
+
- **Architecture adapter.** `Arch` dataclass that handles per-variant differences (layer count, global-attention pattern, RoPE parameters, etc.). Currently supports Gemma 4 E4B and E2B; the adapter pattern follows TransformerLens 3's `TransformerBridge`.
|
|
11
|
+
- **Analysis helpers.** Logit lens, direct logit attribution (`accumulated_resid`, `decompose_resid`, `head_results`, `logit_attrs`), fact vectors, centroid decoding, probe primitives, head-weight static analysis, geometry metrics.
|
|
12
|
+
- **Plot helpers.** Matplotlib conventions baked in for quick diagnostic figures — not the full visualization surface (that lives in `mechbench-ui`).
|
|
13
|
+
|
|
14
|
+
See [`PACKAGE_README.md`](PACKAGE_README.md) for the full API tour and worked examples.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install mechbench-compute
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
From source:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/mechbench/mechbench-compute.git
|
|
26
|
+
cd mechbench-compute
|
|
27
|
+
pip install -e '.[dev]'
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Apple Silicon required (MLX is the only supported backend today). A PyTorch backend would live as `mechbench_compute.backends.torch` alongside the MLX one if/when the need arises; splitting repos by backend is explicitly not planned.
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from mechbench_compute import Model, Ablate, Capture
|
|
36
|
+
|
|
37
|
+
model = Model.load()
|
|
38
|
+
ids = model.tokenize("Complete this sentence with one word: The Eiffel Tower is in")
|
|
39
|
+
|
|
40
|
+
result = model.run(ids)
|
|
41
|
+
for tok, p in result.top_k(model.tokenizer, k=5):
|
|
42
|
+
print(f"{tok!r:20s} p={p:.4f}")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Distributional-target training (`distill` + `lora`)
|
|
46
|
+
|
|
47
|
+
Primitives for training a model toward a specified *distribution* over
|
|
48
|
+
responses rather than toward example responses (task
|
|
49
|
+
`000114`):
|
|
50
|
+
soft-target cross-entropy at decision tokens has gradient P − T, so the
|
|
51
|
+
adapter learns to *emit the distribution*.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import mlx.nn as nn
|
|
55
|
+
import mlx.optimizers as optim
|
|
56
|
+
import numpy as np
|
|
57
|
+
from mechbench_compute import Model, distill, lora
|
|
58
|
+
from mechbench_compute.distill import TargetMap
|
|
59
|
+
|
|
60
|
+
model = Model.load()
|
|
61
|
+
tok = model.tokenizer
|
|
62
|
+
|
|
63
|
+
# A target is a Map<String, Double> — hardcoded, from JSON, or uniform —
|
|
64
|
+
# with whole-map transforms that each return a new map:
|
|
65
|
+
target = TargetMap.from_json("weights.json").sqrt().normalize()
|
|
66
|
+
target = TargetMap.uniform([str(i) for i in range(1, 7)]) # fair d6
|
|
67
|
+
|
|
68
|
+
# Compile it against the rendered prompt: items become token paths
|
|
69
|
+
# (multi-token items share trie nodes; a closer appends continuation
|
|
70
|
+
# anchors so the flattening can't leak past the envelope):
|
|
71
|
+
prompt = distill.render_chat(tok, system, "Please roll the die.",
|
|
72
|
+
prefill='{ "roll": ')
|
|
73
|
+
trie = target.tokenize(tok, prompt, closer=" }")
|
|
74
|
+
|
|
75
|
+
n = lora.apply_lora(model.lm) # freeze + wrap q/v projections
|
|
76
|
+
step = nn.value_and_grad(model.lm, distill.soft_ce)
|
|
77
|
+
opt = optim.Adam(learning_rate=1e-4)
|
|
78
|
+
rng = np.random.default_rng(7)
|
|
79
|
+
for _ in range(steps):
|
|
80
|
+
batch = [trie.hard_example(trie.sample(rng)) for _ in range(3)]
|
|
81
|
+
batch.append(trie.marginal_example()) # exact first-token marginal
|
|
82
|
+
batch.append(sharp_anchor) # keeps confident tasks sharp
|
|
83
|
+
loss, grads = step(model.lm, batch)
|
|
84
|
+
opt.update(model.lm, grads)
|
|
85
|
+
|
|
86
|
+
lora.save_adapter(model.lm, "adapter.safetensors")
|
|
87
|
+
# Later, on a fresh model: merge + exact undo
|
|
88
|
+
handle = lora.fuse(model.lm, lora.load_adapter("adapter.safetensors"),
|
|
89
|
+
scale=16 / 8) # alpha / rank from training
|
|
90
|
+
lora.restore(model.lm, handle)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Calibration is measured at item level (`trie.score`, `distill.item_metrics`
|
|
94
|
+
— captured mass, entropy, KL from target) and at the decision token
|
|
95
|
+
(`distill.first_token_metrics`). `python -m mechbench_compute._smoke_distill`
|
|
96
|
+
runs the full lifecycle on E2B.
|
|
97
|
+
|
|
98
|
+
**Two forward paths.** Training and scoring call `Model.lm` (the text
|
|
99
|
+
decoder, uniform across families) directly — plain module calls,
|
|
100
|
+
differentiable, no instrumentation. `Model.run` remains the hook-aware
|
|
101
|
+
forward for capture/patch/lens work. Adapters bridge the two: `fuse` an
|
|
102
|
+
adapter into the weights and every instrumented run sees the adapted
|
|
103
|
+
model; `restore` flips it back, so base-vs-adapted comparisons run in one
|
|
104
|
+
script.
|
|
105
|
+
|
|
106
|
+
**Scoring tiers** (task 000227): `score_items` is the sequential
|
|
107
|
+
reference oracle; `score_items_batched` adds length-bucketed batching
|
|
108
|
+
(~1.5×); `score_items_fast` additionally splits the forward via
|
|
109
|
+
`Model.trunk_hidden` / `Model.head_logits` and unembeds only the
|
|
110
|
+
supervised rows (~1.5–2.1× vs oracle, family-dependent — best when
|
|
111
|
+
items share no prefix, e.g. cross-document scoring);
|
|
112
|
+
`score_items_cached` encodes a shared prompt **once** into a KV cache
|
|
113
|
+
(`Model.prompt_cache`) and scores each item's 1–4 suffix tokens against
|
|
114
|
+
a per-item copy — the tier for shared-prompt batteries. Measured
|
|
115
|
+
(flat name batteries, cached vs oracle): E2B 2.8×, Gemma-3-4B 3.4×,
|
|
116
|
+
Qwen-3B 2.6×, Llama-8B 3.0×; positions/attention exact by construction,
|
|
117
|
+
bf16 envelope from decomposed attention: mass-region |ΔlogP| ≤ 0.45,
|
|
118
|
+
renormalized KL ≤ 2.4e-2 bits, idempotent (0.0 across repeat calls).
|
|
119
|
+
Fast-tier envelope: max |ΔlogP| ≤ 0.99 (deep tail) / ≤ 0.24
|
|
120
|
+
(mass region), renorm KL ≤ 6.5e-3 bits. All of it is bf16 matmul
|
|
121
|
+
tiling — same rows, same math, verified bit-exact where shapes match.
|
|
122
|
+
Flat-target KL diagnostics weight the tail, so switch tiers only
|
|
123
|
+
between comparisons, never mid-experiment. Known upstream limit
|
|
124
|
+
(mlx 0.31.2 / mlx-lm 0.31.3 / mlx-vlm 0.6.1): **batched cached decoding
|
|
125
|
+
corrupts every batch row after the first** on both stacks (reproduced
|
|
126
|
+
with natively built B=4 caches and identical rows), which is why the
|
|
127
|
+
cached tier is per-item; batched suffix scoring behind an upstream fix
|
|
128
|
+
is the remaining ~5–10× path.
|
|
129
|
+
|
|
130
|
+
## Status
|
|
131
|
+
|
|
132
|
+
The `Arch` adapter supports Gemma 4 E4B and E2B; generalization to other architecture families is ongoing.
|
|
133
|
+
|
|
134
|
+
The substrate epic that will define how intermediate results are cached and shared across experiments is `000162` (DAG solver + content-addressed memoization). It consumes the canonical-serialization guarantee from `000161` (binary formats) and the path grammar from `000163` (identity scheme).
|
|
135
|
+
|
|
136
|
+
## Relationship to other mechbench repos
|
|
137
|
+
|
|
138
|
+
- **`mechbench-schema`** — the typed emission contract. `mechbench-compute` emits records shaped by schema types.
|
|
139
|
+
- **`mechbench-experiments`** — research scripts and findings that consume this package. Uses `mechbench-compute` as its primary dependency.
|
|
140
|
+
- **`mechbench-runner`** — exposes these primitives as agent-callable tools. Imports `mechbench-compute`.
|
|
141
|
+
- **`mechbench-ui`** — TypeScript frontend. Does not import `mechbench-compute` directly; reads bundles produced by it through the `mechbench-schema` contract.
|
|
142
|
+
|
|
143
|
+
See [mechbench.ai](https://mechbench.ai) for the family overview and the design principles.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""mechbench_compute — a mechanistic-interpretability framework for
|
|
2
|
+
Google's Gemma 4 E4B running locally on Apple Silicon via MLX.
|
|
3
|
+
|
|
4
|
+
Quick start:
|
|
5
|
+
|
|
6
|
+
from mechbench_compute import Model
|
|
7
|
+
|
|
8
|
+
model = Model.load("mlx-community/gemma-4-E4B-it-bf16")
|
|
9
|
+
ids = model.tokenize("Complete this sentence with one word: The Eiffel Tower is in")
|
|
10
|
+
|
|
11
|
+
# Forward pass, no instrumentation:
|
|
12
|
+
result = model.run(ids)
|
|
13
|
+
for tok, p in result.top_k(model.tokenizer, k=5):
|
|
14
|
+
print(f'{tok!r:20s} p={p:.4f}')
|
|
15
|
+
|
|
16
|
+
Capture activations:
|
|
17
|
+
|
|
18
|
+
result = model.run(ids, capture=['blocks.23.attn.weights',
|
|
19
|
+
'blocks.14.mlp_out'])
|
|
20
|
+
weights = result.cache['blocks.23.attn.weights'] # [1, 8, S, S], bf16
|
|
21
|
+
mlp = result.cache['blocks.14.mlp_out'] # [1, S, 2560], bf16
|
|
22
|
+
|
|
23
|
+
Modify activations with a hook:
|
|
24
|
+
|
|
25
|
+
import mlx.core as mx
|
|
26
|
+
|
|
27
|
+
def zero_layer_14_mlp(act, info):
|
|
28
|
+
return mx.zeros_like(act)
|
|
29
|
+
|
|
30
|
+
result = model.run(ids, hooks={'blocks.14.mlp_out': zero_layer_14_mlp})
|
|
31
|
+
|
|
32
|
+
Declarative interventions (Ablate / Capture / Patch), prompt tooling,
|
|
33
|
+
logit-lens + geometry helpers, and matplotlib plot helpers are all
|
|
34
|
+
re-exported from this module — see README.md for the full API tour.
|
|
35
|
+
|
|
36
|
+
The full list of hook points is at mechbench_compute.all_hook_names().
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# Everything below the substrate check assumes a substrate. Rather than
|
|
40
|
+
# refusing to import at all, the package now *loads* anywhere and refuses
|
|
41
|
+
# on use — because the modules that report on a machine with no backend
|
|
42
|
+
# (`backends`, `inventory`) live inside this package, and a gate at
|
|
43
|
+
# import time made them reachable only from a machine that did not need
|
|
44
|
+
# them. `doctor` is exactly that machine's tool.
|
|
45
|
+
from .backends import (
|
|
46
|
+
BACKENDS,
|
|
47
|
+
Backend,
|
|
48
|
+
active as active_backend,
|
|
49
|
+
available as available_backends,
|
|
50
|
+
describe_platform,
|
|
51
|
+
require as _require_backend,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if active_backend() is not None:
|
|
55
|
+
from ._arch import (
|
|
56
|
+
Arch,
|
|
57
|
+
D_MODEL,
|
|
58
|
+
GLOBAL_LAYERS,
|
|
59
|
+
LAYER_HOOK_POINTS,
|
|
60
|
+
N_HEADS,
|
|
61
|
+
N_LAYERS,
|
|
62
|
+
VOCAB_SIZE,
|
|
63
|
+
all_hook_names,
|
|
64
|
+
layer_type,
|
|
65
|
+
)
|
|
66
|
+
from .cache import ActivationCache
|
|
67
|
+
from .errors import (
|
|
68
|
+
CacheKeyError,
|
|
69
|
+
InterpError,
|
|
70
|
+
InvalidHookName,
|
|
71
|
+
LayerIndexOutOfRange,
|
|
72
|
+
)
|
|
73
|
+
from .geometry import (
|
|
74
|
+
VocabConcentration,
|
|
75
|
+
centroid_decode,
|
|
76
|
+
cluster_purity,
|
|
77
|
+
cohesion,
|
|
78
|
+
cosine_matrix,
|
|
79
|
+
effective_vocab_size,
|
|
80
|
+
entropy_bits,
|
|
81
|
+
fact_vectors,
|
|
82
|
+
fact_vectors_at,
|
|
83
|
+
fact_vectors_at_hook,
|
|
84
|
+
fact_vectors_pooled,
|
|
85
|
+
intra_inter_separation,
|
|
86
|
+
iterate_clusters,
|
|
87
|
+
nearest_neighbor_purity,
|
|
88
|
+
orthogonalize_against,
|
|
89
|
+
silhouette_cosine,
|
|
90
|
+
top_k_mass,
|
|
91
|
+
vocab_concentration,
|
|
92
|
+
)
|
|
93
|
+
from .generate import generate_labeled_corpus, generate_text
|
|
94
|
+
from .head_weights import (
|
|
95
|
+
CircuitAnalysis,
|
|
96
|
+
CircuitComponent,
|
|
97
|
+
HeadSpec,
|
|
98
|
+
PositionWrite,
|
|
99
|
+
get_head_spec,
|
|
100
|
+
head_key_tokens,
|
|
101
|
+
head_ov_actual_writes,
|
|
102
|
+
head_ov_position_writes,
|
|
103
|
+
head_read_tokens,
|
|
104
|
+
ov_circuit,
|
|
105
|
+
qk_circuit,
|
|
106
|
+
)
|
|
107
|
+
from .attribution import (
|
|
108
|
+
accumulated_resid,
|
|
109
|
+
decompose_resid,
|
|
110
|
+
head_results,
|
|
111
|
+
logit_attrs,
|
|
112
|
+
)
|
|
113
|
+
from .probes import Probe
|
|
114
|
+
from .hooks import HookFn, HookInfo, parse_hook_name
|
|
115
|
+
from .interventions import Ablate, Capture, Intervention, Patch, compose
|
|
116
|
+
from .lens import logit_lens_final, logit_lens_per_position
|
|
117
|
+
from .model import Model, RunResult
|
|
118
|
+
from .plot import (
|
|
119
|
+
bar_by_layer,
|
|
120
|
+
grouped_row_heatmap,
|
|
121
|
+
head_heatmap,
|
|
122
|
+
intensity_curve,
|
|
123
|
+
leaderboard_bar,
|
|
124
|
+
lens_trajectory,
|
|
125
|
+
logprob_trajectory,
|
|
126
|
+
pca_scatter,
|
|
127
|
+
position_heatmap,
|
|
128
|
+
probe_diagonal_heatmap,
|
|
129
|
+
similarity_heatmap,
|
|
130
|
+
)
|
|
131
|
+
from .prompts import (
|
|
132
|
+
Prompt,
|
|
133
|
+
PromptSet,
|
|
134
|
+
ValidatedPrompt,
|
|
135
|
+
ValidatedPromptSet,
|
|
136
|
+
)
|
|
137
|
+
from .distill import (
|
|
138
|
+
Example,
|
|
139
|
+
TargetMap,
|
|
140
|
+
TargetTrie,
|
|
141
|
+
first_token_metrics,
|
|
142
|
+
item_metrics,
|
|
143
|
+
render_chat,
|
|
144
|
+
score_items,
|
|
145
|
+
score_items_batched,
|
|
146
|
+
score_items_cached,
|
|
147
|
+
score_items_fast,
|
|
148
|
+
soft_ce,
|
|
149
|
+
)
|
|
150
|
+
from .lora import (
|
|
151
|
+
LoRALinear,
|
|
152
|
+
apply_lora,
|
|
153
|
+
fuse,
|
|
154
|
+
load_adapter,
|
|
155
|
+
restore,
|
|
156
|
+
save_adapter,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
else:
|
|
160
|
+
|
|
161
|
+
def __getattr__(name: str) -> object:
|
|
162
|
+
"""Explain, rather than fail with a missing-module traceback.
|
|
163
|
+
|
|
164
|
+
Reached only on a machine with no compute backend, where every
|
|
165
|
+
name below is genuinely unavailable — so the same explanation is
|
|
166
|
+
the right answer for all of them.
|
|
167
|
+
|
|
168
|
+
Submodules that touch MLX directly (`model`, `protocol`, …) are
|
|
169
|
+
not covered: importing one still raises out of MLX itself. That
|
|
170
|
+
is the situation `mechbench-runner doctor` exists to catch first.
|
|
171
|
+
"""
|
|
172
|
+
if name.startswith("__"):
|
|
173
|
+
raise AttributeError(name)
|
|
174
|
+
|
|
175
|
+
# Submodules that do not touch the substrate stay reachable —
|
|
176
|
+
# `backends` and `inventory` are precisely what reports on a
|
|
177
|
+
# machine like this one, and `from mechbench_compute import
|
|
178
|
+
# inventory` has to keep working. Without this the package
|
|
179
|
+
# attribute hook shadows the submodule and answers "no backend"
|
|
180
|
+
# for a module that never needed one.
|
|
181
|
+
import importlib
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
return importlib.import_module(f".{name}", __name__)
|
|
185
|
+
except ImportError:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
# Either a name that lives behind the substrate, or a submodule
|
|
189
|
+
# that failed on MLX itself. Both get the explanation rather than
|
|
190
|
+
# a traceback out of a package nobody asked for.
|
|
191
|
+
_require_backend()
|
|
192
|
+
raise AttributeError(name) # unreachable; require() always raises
|
|
193
|
+
|
|
194
|
+
__version__ = "0.10.0"
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
# Substrate — the part that answers on a machine with no backend
|
|
198
|
+
"Backend",
|
|
199
|
+
"BACKENDS",
|
|
200
|
+
"active_backend",
|
|
201
|
+
"available_backends",
|
|
202
|
+
"describe_platform",
|
|
203
|
+
# Main API
|
|
204
|
+
"Model",
|
|
205
|
+
"RunResult",
|
|
206
|
+
"ActivationCache",
|
|
207
|
+
# Declarative interventions
|
|
208
|
+
"Ablate",
|
|
209
|
+
"Capture",
|
|
210
|
+
"Patch",
|
|
211
|
+
"Intervention",
|
|
212
|
+
"compose",
|
|
213
|
+
# Prompts (specific prompt collections live in experiments.prompts)
|
|
214
|
+
"Prompt",
|
|
215
|
+
"PromptSet",
|
|
216
|
+
"ValidatedPrompt",
|
|
217
|
+
"ValidatedPromptSet",
|
|
218
|
+
# Distributional-target training (distill + lora, task 000114)
|
|
219
|
+
"TargetMap",
|
|
220
|
+
"TargetTrie",
|
|
221
|
+
"Example",
|
|
222
|
+
"soft_ce",
|
|
223
|
+
"render_chat",
|
|
224
|
+
"score_items",
|
|
225
|
+
"score_items_batched",
|
|
226
|
+
"score_items_cached",
|
|
227
|
+
"score_items_fast",
|
|
228
|
+
"item_metrics",
|
|
229
|
+
"first_token_metrics",
|
|
230
|
+
"LoRALinear",
|
|
231
|
+
"apply_lora",
|
|
232
|
+
"save_adapter",
|
|
233
|
+
"load_adapter",
|
|
234
|
+
"fuse",
|
|
235
|
+
"restore",
|
|
236
|
+
# Logit lens
|
|
237
|
+
"logit_lens_final",
|
|
238
|
+
"logit_lens_per_position",
|
|
239
|
+
# Direct logit attribution + residual decomposition
|
|
240
|
+
"accumulated_resid",
|
|
241
|
+
"decompose_resid",
|
|
242
|
+
"head_results",
|
|
243
|
+
"logit_attrs",
|
|
244
|
+
# Fact vectors + geometry
|
|
245
|
+
"fact_vectors",
|
|
246
|
+
"fact_vectors_at",
|
|
247
|
+
"fact_vectors_at_hook",
|
|
248
|
+
"fact_vectors_pooled",
|
|
249
|
+
"centroid_decode",
|
|
250
|
+
"cohesion",
|
|
251
|
+
"cosine_matrix",
|
|
252
|
+
"intra_inter_separation",
|
|
253
|
+
"iterate_clusters",
|
|
254
|
+
"cluster_purity",
|
|
255
|
+
"silhouette_cosine",
|
|
256
|
+
"nearest_neighbor_purity",
|
|
257
|
+
"orthogonalize_against",
|
|
258
|
+
# Probes (persistent concept vectors)
|
|
259
|
+
"Probe",
|
|
260
|
+
# Generation (for corpus-building workflows)
|
|
261
|
+
"generate_text",
|
|
262
|
+
"generate_labeled_corpus",
|
|
263
|
+
# Head-weight analysis (static interp on W_Q/W_K/W_V/W_O)
|
|
264
|
+
"HeadSpec",
|
|
265
|
+
"CircuitComponent",
|
|
266
|
+
"CircuitAnalysis",
|
|
267
|
+
"PositionWrite",
|
|
268
|
+
"get_head_spec",
|
|
269
|
+
"head_read_tokens",
|
|
270
|
+
"head_key_tokens",
|
|
271
|
+
"qk_circuit",
|
|
272
|
+
"ov_circuit",
|
|
273
|
+
"head_ov_position_writes",
|
|
274
|
+
"head_ov_actual_writes",
|
|
275
|
+
# Vocabulary-space concentration
|
|
276
|
+
"VocabConcentration",
|
|
277
|
+
"vocab_concentration",
|
|
278
|
+
"top_k_mass",
|
|
279
|
+
"entropy_bits",
|
|
280
|
+
"effective_vocab_size",
|
|
281
|
+
# Plot helpers
|
|
282
|
+
"bar_by_layer",
|
|
283
|
+
"lens_trajectory",
|
|
284
|
+
"logprob_trajectory",
|
|
285
|
+
"position_heatmap",
|
|
286
|
+
"pca_scatter",
|
|
287
|
+
"similarity_heatmap",
|
|
288
|
+
"head_heatmap",
|
|
289
|
+
"probe_diagonal_heatmap",
|
|
290
|
+
"grouped_row_heatmap",
|
|
291
|
+
"intensity_curve",
|
|
292
|
+
"leaderboard_bar",
|
|
293
|
+
# Hook types (for users writing raw callbacks)
|
|
294
|
+
"HookInfo",
|
|
295
|
+
"HookFn",
|
|
296
|
+
"parse_hook_name",
|
|
297
|
+
# Architecture facts
|
|
298
|
+
"Arch",
|
|
299
|
+
"N_LAYERS",
|
|
300
|
+
"D_MODEL",
|
|
301
|
+
"N_HEADS",
|
|
302
|
+
"VOCAB_SIZE",
|
|
303
|
+
"GLOBAL_LAYERS",
|
|
304
|
+
"LAYER_HOOK_POINTS",
|
|
305
|
+
"layer_type",
|
|
306
|
+
"all_hook_names",
|
|
307
|
+
# Errors
|
|
308
|
+
"InterpError",
|
|
309
|
+
"InvalidHookName",
|
|
310
|
+
"LayerIndexOutOfRange",
|
|
311
|
+
"CacheKeyError",
|
|
312
|
+
]
|