logogram 0.1.0__tar.gz → 0.1.1__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.
- logogram-0.1.1/CHANGELOG.md +60 -0
- logogram-0.1.0/README.md → logogram-0.1.1/PKG-INFO +75 -15
- logogram-0.1.0/PKG-INFO → logogram-0.1.1/README.md +38 -48
- {logogram-0.1.0 → logogram-0.1.1}/pyproject.toml +7 -1
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/__init__.py +1 -1
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/analysis.py +5 -5
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/atp.py +3 -3
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/base.py +15 -2
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/hub.py +54 -7
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/saes.py +10 -10
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/transformer_lens.py +132 -26
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/cli.py +21 -11
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/direct.py +16 -6
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/engine.py +18 -3
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/features.py +8 -8
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/app.py +5 -3
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/models.py +1 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/steering.py +9 -6
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/system.py +20 -7
- logogram-0.1.0/src/logogram/web_dist/assets/index-BvCU-2uy.js → logogram-0.1.1/src/logogram/web_dist/assets/index-CC9_IJY6.js +1 -1
- logogram-0.1.1/src/logogram/web_dist/assets/index-Dt6ecB66.css +1 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/web_dist/index.html +2 -2
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_cli.py +33 -0
- logogram-0.1.1/tests/test_devices.py +50 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_direct.py +38 -0
- logogram-0.1.1/tests/test_loading.py +235 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_units.py +12 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/api/client.ts +1 -1
- {logogram-0.1.0 → logogram-0.1.1}/web/src/api/types.ts +2 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelDialog.module.css +6 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelDialog.tsx +10 -3
- logogram-0.1.0/src/logogram/web_dist/assets/index-DTr8_ucV.css +0 -1
- {logogram-0.1.0 → logogram-0.1.1}/.gitignore +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/AGENTS.md +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/LICENSE +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/scripts/check_privacy.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/scripts/smoke_wheel.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/__main__.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/__init__.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/compare.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/datasets.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/examples/ioi-gpt2/.gitignore +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/examples/ioi-gpt2/datasets/ioi.jsonl +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/examples/ioi-gpt2/experiments/ioi-head-patching/spec.json +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/examples/ioi-gpt2/project.json +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/exports.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/fileio.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/ioi.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/paths.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/project.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/prompts.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/research.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/results.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/runner.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/runs.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/sae.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/schema.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/__init__.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/security.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/state.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/sites.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/spec.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/stats.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/updates.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/verify.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/web_dist/assets/instrument-sans-latin-ext-standard-normal-C5E2Gvlv.woff2 +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/web_dist/assets/instrument-sans-latin-standard-normal-BVScPF0l.woff2 +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/src/logogram/web_dist/favicon.svg +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/browser_server.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/conftest.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_architectures.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_atp.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_engine.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_explorer.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_hardening.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_paths.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_privacy.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_release_fixes.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_sae.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_sanity.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_server.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_steering.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/tests/test_updates.py +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/uv.lock +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/index.html +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/package-lock.json +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/package.json +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/playwright.config.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/public/favicon.svg +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/App.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/App.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/api/events.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/CommandPalette.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/CommandPalette.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/CopyCommand.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/CopyCommand.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Distribution.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Distribution.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/FolderPicker.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/FolderPicker.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Header.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Header.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Heatmap/Heatmap.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Heatmap/Heatmap.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Heatmap/ScaleBar.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/History.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/History.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Inspector.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Inspector.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Logogram.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/LogogramDial.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/LogogramDial.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelMap/Legend.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelMap/ModelMap.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelMap/ModelMap.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Notices.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Notices.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/RobustnessDialog.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Splitter.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Splitter.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/StatusLine.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/StatusLine.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/TokenStrip.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/TokenStrip.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/TopBar.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/TopBar.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/UpdateNotice.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/UpdateNotice.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ui/Icon.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ui/index.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ui/ui.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/analysis.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/canvas.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/color.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/export.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/format.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/heatmapNavigation.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/hooks.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/keys.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/logogram.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/sites.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/spec.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/main.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Projects.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Screens.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/SystemCheck.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Welcome.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Workbench.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Workbench.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/store/app.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/styles/global.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/styles/tokens.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/AttentionView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/AttentionView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/BaselineView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/BaselineView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/CompareView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/CompareView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ExperimentView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ExperimentView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ExploreView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ExploreView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/FeaturesView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/FeaturesView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/Forest.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/Forest.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/HeadComparisonView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/HeadComparisonView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/PredictionsView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/PredictionsView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/PromptsView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ResearchView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ResearchView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ResultsView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ResultsView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/SpecView.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/SpecView.tsx +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/src/views/views.module.css +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/tests/context.unit.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/tests/logogram.unit.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/tests/workbench.browser.ts +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/tsconfig.json +0 -0
- {logogram-0.1.0 → logogram-0.1.1}/web/vite.config.ts +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What changed in each version of Logogram.
|
|
4
|
+
|
|
5
|
+
## 0.1.1 (2026-10-08)
|
|
6
|
+
|
|
7
|
+
Every method has now run end to end on real weights: GPT-2 small with a SAELens and an OpenAI
|
|
8
|
+
SAE, Pythia-70m with an EleutherAI SAE, and Qwen 2.5 0.5B. This release fixes what that turned up.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Runs work on Apple Silicon. Every method converted tensors to float64 on the model's device,
|
|
13
|
+
which Apple's Metal (MPS) doesn't have, so no run got past its baseline on a Mac. This still
|
|
14
|
+
needs a check on a Mac.
|
|
15
|
+
- A model whose own forward pass overflows in the chosen dtype (Pythia in float16) is refused, with
|
|
16
|
+
the dtype to use. Its load checks used to read as passed.
|
|
17
|
+
- When processing a model's weights doesn't fit in memory, loading says so and frees the memory,
|
|
18
|
+
instead of failing with a raw CUDA error and keeping it.
|
|
19
|
+
- The memory estimate counts weight processing, which needs three to four times the float32 size
|
|
20
|
+
of the weights while the model loads. Qwen 2.5 0.5B on a 6 GB GPU now reads "won't fit" with
|
|
21
|
+
processing and "fits" without it.
|
|
22
|
+
- Models without a beginning-of-sequence token, such as Qwen, run without one, as documented.
|
|
23
|
+
TransformerLens 4 had given them its end-of-text token instead.
|
|
24
|
+
- Models and SAEs that Logogram downloaded open offline without a revision, and offline memory
|
|
25
|
+
estimates use the model's real size.
|
|
26
|
+
- `logogram run` reports an identical rerun of direct logit attribution or attribution patching as
|
|
27
|
+
identical.
|
|
28
|
+
- Direct logit attribution in float16 and bfloat16 no longer warns about drift that is only
|
|
29
|
+
rounding.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Runs warn when the model prefers the distractor on the clean prompts, so it doesn't show the
|
|
34
|
+
behavior they test (Pythia-70m on IOI).
|
|
35
|
+
- The model dialog shows the memory that weight processing needs. Qwen 2.5 0.5B is marked as
|
|
36
|
+
tested.
|
|
37
|
+
- Project links on PyPI, and this changelog, which the update notice links to.
|
|
38
|
+
|
|
39
|
+
### Documentation
|
|
40
|
+
|
|
41
|
+
- What has been validated with real weights, and where attribution patching is least reliable:
|
|
42
|
+
residual stream sites at the token that differs between the prompts.
|
|
43
|
+
- Steering needs prompt pairs that differ the same way; IOI prompts that mix the ABBA and BABA
|
|
44
|
+
orders cancel out.
|
|
45
|
+
- What float16 and bfloat16 do to results, and how weight processing changes heads' direct effects.
|
|
46
|
+
|
|
47
|
+
## 0.1.0 (2026-10-08)
|
|
48
|
+
|
|
49
|
+
First public release.
|
|
50
|
+
|
|
51
|
+
- Activation patching, ablation (zero, mean or resample), direct logit attribution, attribution
|
|
52
|
+
patching verified by patching, steering with a random control, path patching, and SAE features,
|
|
53
|
+
all described by one spec that the app and `logogram run` execute the same way.
|
|
54
|
+
- Every model is checked when it loads: that TransformerLens reproduces its predictions, how its
|
|
55
|
+
layers add into the residual stream, and whether the logit lens reproduces its output.
|
|
56
|
+
- Per-prompt values with seeded bootstrap intervals, and bit-identical reruns on the same machine.
|
|
57
|
+
- The workbench: Explore, Experiment and Evidence, logograms written by results, robustness checks
|
|
58
|
+
and run comparisons.
|
|
59
|
+
- Local-first: no account and no telemetry, safetensors weights only, and an update check that
|
|
60
|
+
runs only when you allow it.
|
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: logogram
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A local-first workbench for causal experiments inside language models.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Jeevash23/logogram
|
|
6
|
+
Project-URL: Source, https://github.com/Jeevash23/logogram
|
|
7
|
+
Project-URL: Issues, https://github.com/Jeevash23/logogram/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Jeevash23/logogram/blob/main/CHANGELOG.md
|
|
9
|
+
Author: The Logogram contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: activation-patching,interpretability,mechanistic-interpretability,transformers
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: anyio>=4
|
|
23
|
+
Requires-Dist: fastapi>=0.115
|
|
24
|
+
Requires-Dist: huggingface-hub>=0.23
|
|
25
|
+
Requires-Dist: numpy>=1.26
|
|
26
|
+
Requires-Dist: platformdirs>=4
|
|
27
|
+
Requires-Dist: psutil>=5.9
|
|
28
|
+
Requires-Dist: pyarrow>=15
|
|
29
|
+
Requires-Dist: pydantic>=2.7
|
|
30
|
+
Requires-Dist: torch>=2.4
|
|
31
|
+
Requires-Dist: transformer-lens<5,>=4.0
|
|
32
|
+
Requires-Dist: transformers>=4.45
|
|
33
|
+
Requires-Dist: typer>=0.12
|
|
34
|
+
Requires-Dist: uvicorn>=0.30
|
|
35
|
+
Requires-Dist: websockets>=12
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
1
38
|
# Logogram
|
|
2
39
|
|
|
3
40
|
A local workbench for causal experiments inside language models.
|
|
@@ -25,9 +62,10 @@ Logogram needs Python 3.11 or newer and [uv](https://docs.astral.sh/uv/).
|
|
|
25
62
|
uv tool install logogram
|
|
26
63
|
```
|
|
27
64
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
65
|
+
To run the code in a clone of this repository instead, install it with `uv tool install .`; the
|
|
66
|
+
commands in the table below work the same way with `.` in place of `logogram`. The web app is
|
|
67
|
+
prebuilt inside the package, so you never need Node. What changed in each version is in
|
|
68
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
31
69
|
|
|
32
70
|
PyTorch is chosen per machine:
|
|
33
71
|
|
|
@@ -229,7 +267,11 @@ estimate misses saturation (in attention, normalization and the final softmax) a
|
|
|
229
267
|
even invert an effect, so **Verify top 10 by patching** on the results page patches the sites
|
|
230
268
|
with the largest estimated effects for real and opens the comparison: the rank correlation, and
|
|
231
269
|
any estimate whose sign patching confidently reverses. **Check robustness** can also patch the
|
|
232
|
-
whole sweep.
|
|
270
|
+
whole sweep. Do that for residual stream sweeps: an estimate is least reliable where patching
|
|
271
|
+
replaces a whole token's representation. In the IOI example on GPT-2 small, patching the residual
|
|
272
|
+
stream at the changed name in the first layer restores the whole answer while its estimate is
|
|
273
|
+
slightly negative, so verifying the top estimates would never reach it. Head outputs track
|
|
274
|
+
patching closely.
|
|
233
275
|
|
|
234
276
|
**Direct logit attribution** splits the logit difference of the clean or the corrupt prompts
|
|
235
277
|
(your choice; there is no default) into what each head, attention output and MLP output writes
|
|
@@ -266,7 +308,10 @@ like patching, so 1 means the steered prompts moved as far as switching to the o
|
|
|
266
308
|
random direction of the same length, at the same strengths, runs alongside as a control, and the
|
|
267
309
|
results show both: a layer × strength map with the control columns muted, and, for a selected site,
|
|
268
310
|
its effect at every strength with intervals. **Check robustness** offers another split of the pairs
|
|
269
|
-
or the other direction.
|
|
311
|
+
or the other direction. A mean difference steers only when the pairs differ the same way. In IOI
|
|
312
|
+
prompts that mix the ABBA and BABA orders, the difference at the last token flips with the order,
|
|
313
|
+
so the mean cancels and steering does no more than the control; generate prompts of one order to
|
|
314
|
+
steer.
|
|
270
315
|
|
|
271
316
|
**SAE features.** A sparse autoencoder (SAE) rewrites one of the model's activations as a few active
|
|
272
317
|
features out of thousands, each a direction in the model, plus an error it misses. In
|
|
@@ -335,7 +380,7 @@ Every experiment is a `spec.json`. Nothing that can change a number is left impl
|
|
|
335
380
|
| Field | Values |
|
|
336
381
|
|---|---|
|
|
337
382
|
| `model.revision` | A commit. `null` resolves the current main branch at run time; the saved spec pins what ran. |
|
|
338
|
-
| `model.process_weights` | Fold LayerNorm and center weights, as TransformerLens does by default. Logit differences don't change
|
|
383
|
+
| `model.process_weights` | Fold LayerNorm and center weights, as TransformerLens does by default. Logit differences don't change. Value biases are folded into the attention output's bias, so zero ablation of head outputs and heads' direct effects do change (by up to several logits in Qwen 2.5, whose value biases are large); a layer's attention output doesn't. |
|
|
339
384
|
| `dataset.sha256` | If set, the run refuses a dataset file that has changed. |
|
|
340
385
|
| `experiment` | `{"kind": "activation_patching", "direction": "clean_to_corrupt" \| "corrupt_to_clean"}`, `{"kind": "ablation", "baseline": …}` with `{"kind": "zero"}`, `{"kind": "mean", "reference": "clean" \| "corrupt"}` or `{"kind": "resample", "pool": "clean" \| "corrupt", "donors": 10, "seed": 0}`, `{"kind": "attribution_patching", "direction": …}` (same directions as patching), `{"kind": "direct_logit_attribution", "prompts": "clean" \| "corrupt"}`, `{"kind": "path_patching", "direction": …, "receivers": [{"kind": "head", "layer": 9, "head": 9, "input": "q" \| "k" \| "v"}, {"kind": "logits"}], "freeze_mlps": false}`, or `{"kind": "steering", "apply_to": "clean" \| "corrupt", "coefficients": [-1, 1, 2], "train_fraction": 0.5, "seed": 0, "control": true}` with a scope of one residual component per layer (`layer_components`) or residual `sites`, at one token |
|
|
341
386
|
| `scope` | `{"kind": "heads", "position": …}`, `{"kind": "layer_position", "site": "resid_pre", "positions": "each" \| "labels"}`, `{"kind": "layer_components", "components": ["attn_out", "mlp_out"], "position": …}` `{"kind": "sites", "sites": [{"kind": "head", "layer": 9, "head": 9, "position": {"kind": "label", "label": "end"}}]}` (feature sites are `{"kind": "sae_feature", "layer": 8, "feature": 1234, "position": …}`), or, for attribution patching, `{"kind": "features", "position": …, "top": 50}` |
|
|
@@ -435,12 +480,23 @@ TransformerLens 4 loads well over a hundred architectures, and Logogram works wi
|
|
|
435
480
|
decoder-only language models among them: GPT-2, Pythia, Llama, Mistral, SmolLM, Qwen, Gemma,
|
|
436
481
|
OLMo, Phi and more. The model dialog lists starting points, and any other Hugging Face id can be
|
|
437
482
|
typed in. Before downloading, Logogram checks that TransformerLens supports the architecture and
|
|
438
|
-
estimates the memory needed (weights, activations and a margin) against what is free.
|
|
483
|
+
estimates the memory needed (weights, activations and a margin) against what is free. Processing
|
|
484
|
+
the weights needs more for a moment while the model loads: TransformerLens works on float32
|
|
485
|
+
copies, three to four times the float32 size of the weights. The estimate includes them, so a
|
|
486
|
+
model that fits only without processing (Qwen 2.5 0.5B on a 6 GB GPU) says so before it loads.
|
|
487
|
+
|
|
488
|
+
Use float32 whenever the model fits. float16 and bfloat16 halve the memory and run two to three
|
|
489
|
+
times faster on a GPU, but they round. In the IOI example on GPT-2 small and Qwen 2.5, float16
|
|
490
|
+
moved effects by at most 0.003 and bfloat16 by up to 0.03: the strongest sites stayed in place,
|
|
491
|
+
but effects smaller than about 0.01 changed order. On Pythia-70m, bfloat16 doubled the clean
|
|
492
|
+
logit difference and reordered the heads, and float16 overflows. Check results that matter
|
|
493
|
+
against float32.
|
|
439
494
|
|
|
440
495
|
Rather than trusting a list, Logogram checks every model when it loads, on a short fixed input:
|
|
441
496
|
|
|
442
497
|
* TransformerLens's version of the model must predict what the original model predicts. If weight
|
|
443
|
-
processing changes the predictions, loading with processed weights is refused.
|
|
498
|
+
processing changes the predictions, loading with processed weights is refused. So is a dtype in
|
|
499
|
+
which the model overflows (Pythia in float16).
|
|
444
500
|
* It measures how each layer adds attention and the MLP to the residual stream: one after the
|
|
445
501
|
other (sequential), or both from the same input (parallel, as in Pythia, GPT-J and Phi). Only
|
|
446
502
|
sequential layers have a residual stream between attention and MLP (`resid_mid`), and the layer
|
|
@@ -448,11 +504,15 @@ Rather than trusting a list, Logogram checks every model when it loads, on a sho
|
|
|
448
504
|
* Layer predictions are offered when the final normalization and unembedding, with any logit
|
|
449
505
|
soft-capping, reproduce the model's output.
|
|
450
506
|
|
|
451
|
-
GPT-2 small is the model the bundled example was made for
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
507
|
+
GPT-2 small is the model the bundled example was made for. Every method has been run end to end
|
|
508
|
+
with real weights on GPT-2 small (with a SAELens and an OpenAI SAE), Pythia-70m (with an
|
|
509
|
+
EleutherAI SAE) and Qwen 2.5 0.5B, and the test suite runs the sanity checks on tiny random models
|
|
510
|
+
of the Llama, Pythia, Qwen 2, Gemma 2 and OLMo 2 families without downloading anything. The
|
|
511
|
+
example's names are single tokens for GPT-2 and Qwen but not all for Pythia; for another model,
|
|
512
|
+
generate IOI prompts in the app, which keeps only names that are single tokens for it. A small
|
|
513
|
+
model may not do the task at all (Pythia-70m prefers the repeated name), and its runs then say so.
|
|
514
|
+
Pythia publishes checkpoints from throughout training as revisions (`step1000` to `step143000`),
|
|
515
|
+
so an experiment can be rerun at several points of training.
|
|
456
516
|
|
|
457
517
|
Some tokenizers, such as Qwen's, have no beginning-of-sequence token. Logogram then runs prompts
|
|
458
518
|
without one, and the spec records it. Answers and distractors must still be single tokens.
|
|
@@ -503,8 +563,8 @@ Publishing, so no token is stored anywhere. PyPI never accepts the same version
|
|
|
503
563
|
|
|
504
564
|
Before tagging, manually check a first GPT-2 download, cancel and retry it, then run the example
|
|
505
565
|
on each supported compute backend. CPU and CUDA are covered by local development checks; MPS
|
|
506
|
-
still needs a check on Apple Silicon.
|
|
507
|
-
|
|
566
|
+
still needs a check on Apple Silicon. Pythia and Qwen 2.5 have also been run end to end with
|
|
567
|
+
their real weights; other families are checked when they load and in the tiny-model tests.
|
|
508
568
|
|
|
509
569
|
Model access goes through `logogram.backends.base.ModelBackend`. TransformerLens is the only
|
|
510
570
|
backend today; remote execution and other libraries can be added behind the same interface.
|
|
@@ -1,36 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: logogram
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: A local-first workbench for causal experiments inside language models.
|
|
5
|
-
Author: The Logogram contributors
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Keywords: activation-patching,interpretability,mechanistic-interpretability,transformers
|
|
9
|
-
Classifier: Development Status :: 4 - Beta
|
|
10
|
-
Classifier: Intended Audience :: Science/Research
|
|
11
|
-
Classifier: Operating System :: OS Independent
|
|
12
|
-
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
-
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
-
Requires-Python: >=3.11
|
|
18
|
-
Requires-Dist: anyio>=4
|
|
19
|
-
Requires-Dist: fastapi>=0.115
|
|
20
|
-
Requires-Dist: huggingface-hub>=0.23
|
|
21
|
-
Requires-Dist: numpy>=1.26
|
|
22
|
-
Requires-Dist: platformdirs>=4
|
|
23
|
-
Requires-Dist: psutil>=5.9
|
|
24
|
-
Requires-Dist: pyarrow>=15
|
|
25
|
-
Requires-Dist: pydantic>=2.7
|
|
26
|
-
Requires-Dist: torch>=2.4
|
|
27
|
-
Requires-Dist: transformer-lens<5,>=4.0
|
|
28
|
-
Requires-Dist: transformers>=4.45
|
|
29
|
-
Requires-Dist: typer>=0.12
|
|
30
|
-
Requires-Dist: uvicorn>=0.30
|
|
31
|
-
Requires-Dist: websockets>=12
|
|
32
|
-
Description-Content-Type: text/markdown
|
|
33
|
-
|
|
34
1
|
# Logogram
|
|
35
2
|
|
|
36
3
|
A local workbench for causal experiments inside language models.
|
|
@@ -58,9 +25,10 @@ Logogram needs Python 3.11 or newer and [uv](https://docs.astral.sh/uv/).
|
|
|
58
25
|
uv tool install logogram
|
|
59
26
|
```
|
|
60
27
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
28
|
+
To run the code in a clone of this repository instead, install it with `uv tool install .`; the
|
|
29
|
+
commands in the table below work the same way with `.` in place of `logogram`. The web app is
|
|
30
|
+
prebuilt inside the package, so you never need Node. What changed in each version is in
|
|
31
|
+
[CHANGELOG.md](CHANGELOG.md).
|
|
64
32
|
|
|
65
33
|
PyTorch is chosen per machine:
|
|
66
34
|
|
|
@@ -262,7 +230,11 @@ estimate misses saturation (in attention, normalization and the final softmax) a
|
|
|
262
230
|
even invert an effect, so **Verify top 10 by patching** on the results page patches the sites
|
|
263
231
|
with the largest estimated effects for real and opens the comparison: the rank correlation, and
|
|
264
232
|
any estimate whose sign patching confidently reverses. **Check robustness** can also patch the
|
|
265
|
-
whole sweep.
|
|
233
|
+
whole sweep. Do that for residual stream sweeps: an estimate is least reliable where patching
|
|
234
|
+
replaces a whole token's representation. In the IOI example on GPT-2 small, patching the residual
|
|
235
|
+
stream at the changed name in the first layer restores the whole answer while its estimate is
|
|
236
|
+
slightly negative, so verifying the top estimates would never reach it. Head outputs track
|
|
237
|
+
patching closely.
|
|
266
238
|
|
|
267
239
|
**Direct logit attribution** splits the logit difference of the clean or the corrupt prompts
|
|
268
240
|
(your choice; there is no default) into what each head, attention output and MLP output writes
|
|
@@ -299,7 +271,10 @@ like patching, so 1 means the steered prompts moved as far as switching to the o
|
|
|
299
271
|
random direction of the same length, at the same strengths, runs alongside as a control, and the
|
|
300
272
|
results show both: a layer × strength map with the control columns muted, and, for a selected site,
|
|
301
273
|
its effect at every strength with intervals. **Check robustness** offers another split of the pairs
|
|
302
|
-
or the other direction.
|
|
274
|
+
or the other direction. A mean difference steers only when the pairs differ the same way. In IOI
|
|
275
|
+
prompts that mix the ABBA and BABA orders, the difference at the last token flips with the order,
|
|
276
|
+
so the mean cancels and steering does no more than the control; generate prompts of one order to
|
|
277
|
+
steer.
|
|
303
278
|
|
|
304
279
|
**SAE features.** A sparse autoencoder (SAE) rewrites one of the model's activations as a few active
|
|
305
280
|
features out of thousands, each a direction in the model, plus an error it misses. In
|
|
@@ -368,7 +343,7 @@ Every experiment is a `spec.json`. Nothing that can change a number is left impl
|
|
|
368
343
|
| Field | Values |
|
|
369
344
|
|---|---|
|
|
370
345
|
| `model.revision` | A commit. `null` resolves the current main branch at run time; the saved spec pins what ran. |
|
|
371
|
-
| `model.process_weights` | Fold LayerNorm and center weights, as TransformerLens does by default. Logit differences don't change
|
|
346
|
+
| `model.process_weights` | Fold LayerNorm and center weights, as TransformerLens does by default. Logit differences don't change. Value biases are folded into the attention output's bias, so zero ablation of head outputs and heads' direct effects do change (by up to several logits in Qwen 2.5, whose value biases are large); a layer's attention output doesn't. |
|
|
372
347
|
| `dataset.sha256` | If set, the run refuses a dataset file that has changed. |
|
|
373
348
|
| `experiment` | `{"kind": "activation_patching", "direction": "clean_to_corrupt" \| "corrupt_to_clean"}`, `{"kind": "ablation", "baseline": …}` with `{"kind": "zero"}`, `{"kind": "mean", "reference": "clean" \| "corrupt"}` or `{"kind": "resample", "pool": "clean" \| "corrupt", "donors": 10, "seed": 0}`, `{"kind": "attribution_patching", "direction": …}` (same directions as patching), `{"kind": "direct_logit_attribution", "prompts": "clean" \| "corrupt"}`, `{"kind": "path_patching", "direction": …, "receivers": [{"kind": "head", "layer": 9, "head": 9, "input": "q" \| "k" \| "v"}, {"kind": "logits"}], "freeze_mlps": false}`, or `{"kind": "steering", "apply_to": "clean" \| "corrupt", "coefficients": [-1, 1, 2], "train_fraction": 0.5, "seed": 0, "control": true}` with a scope of one residual component per layer (`layer_components`) or residual `sites`, at one token |
|
|
374
349
|
| `scope` | `{"kind": "heads", "position": …}`, `{"kind": "layer_position", "site": "resid_pre", "positions": "each" \| "labels"}`, `{"kind": "layer_components", "components": ["attn_out", "mlp_out"], "position": …}` `{"kind": "sites", "sites": [{"kind": "head", "layer": 9, "head": 9, "position": {"kind": "label", "label": "end"}}]}` (feature sites are `{"kind": "sae_feature", "layer": 8, "feature": 1234, "position": …}`), or, for attribution patching, `{"kind": "features", "position": …, "top": 50}` |
|
|
@@ -468,12 +443,23 @@ TransformerLens 4 loads well over a hundred architectures, and Logogram works wi
|
|
|
468
443
|
decoder-only language models among them: GPT-2, Pythia, Llama, Mistral, SmolLM, Qwen, Gemma,
|
|
469
444
|
OLMo, Phi and more. The model dialog lists starting points, and any other Hugging Face id can be
|
|
470
445
|
typed in. Before downloading, Logogram checks that TransformerLens supports the architecture and
|
|
471
|
-
estimates the memory needed (weights, activations and a margin) against what is free.
|
|
446
|
+
estimates the memory needed (weights, activations and a margin) against what is free. Processing
|
|
447
|
+
the weights needs more for a moment while the model loads: TransformerLens works on float32
|
|
448
|
+
copies, three to four times the float32 size of the weights. The estimate includes them, so a
|
|
449
|
+
model that fits only without processing (Qwen 2.5 0.5B on a 6 GB GPU) says so before it loads.
|
|
450
|
+
|
|
451
|
+
Use float32 whenever the model fits. float16 and bfloat16 halve the memory and run two to three
|
|
452
|
+
times faster on a GPU, but they round. In the IOI example on GPT-2 small and Qwen 2.5, float16
|
|
453
|
+
moved effects by at most 0.003 and bfloat16 by up to 0.03: the strongest sites stayed in place,
|
|
454
|
+
but effects smaller than about 0.01 changed order. On Pythia-70m, bfloat16 doubled the clean
|
|
455
|
+
logit difference and reordered the heads, and float16 overflows. Check results that matter
|
|
456
|
+
against float32.
|
|
472
457
|
|
|
473
458
|
Rather than trusting a list, Logogram checks every model when it loads, on a short fixed input:
|
|
474
459
|
|
|
475
460
|
* TransformerLens's version of the model must predict what the original model predicts. If weight
|
|
476
|
-
processing changes the predictions, loading with processed weights is refused.
|
|
461
|
+
processing changes the predictions, loading with processed weights is refused. So is a dtype in
|
|
462
|
+
which the model overflows (Pythia in float16).
|
|
477
463
|
* It measures how each layer adds attention and the MLP to the residual stream: one after the
|
|
478
464
|
other (sequential), or both from the same input (parallel, as in Pythia, GPT-J and Phi). Only
|
|
479
465
|
sequential layers have a residual stream between attention and MLP (`resid_mid`), and the layer
|
|
@@ -481,11 +467,15 @@ Rather than trusting a list, Logogram checks every model when it loads, on a sho
|
|
|
481
467
|
* Layer predictions are offered when the final normalization and unembedding, with any logit
|
|
482
468
|
soft-capping, reproduce the model's output.
|
|
483
469
|
|
|
484
|
-
GPT-2 small is the model the bundled example was made for
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
470
|
+
GPT-2 small is the model the bundled example was made for. Every method has been run end to end
|
|
471
|
+
with real weights on GPT-2 small (with a SAELens and an OpenAI SAE), Pythia-70m (with an
|
|
472
|
+
EleutherAI SAE) and Qwen 2.5 0.5B, and the test suite runs the sanity checks on tiny random models
|
|
473
|
+
of the Llama, Pythia, Qwen 2, Gemma 2 and OLMo 2 families without downloading anything. The
|
|
474
|
+
example's names are single tokens for GPT-2 and Qwen but not all for Pythia; for another model,
|
|
475
|
+
generate IOI prompts in the app, which keeps only names that are single tokens for it. A small
|
|
476
|
+
model may not do the task at all (Pythia-70m prefers the repeated name), and its runs then say so.
|
|
477
|
+
Pythia publishes checkpoints from throughout training as revisions (`step1000` to `step143000`),
|
|
478
|
+
so an experiment can be rerun at several points of training.
|
|
489
479
|
|
|
490
480
|
Some tokenizers, such as Qwen's, have no beginning-of-sequence token. Logogram then runs prompts
|
|
491
481
|
without one, and the spec records it. Answers and distractors must still be single tokens.
|
|
@@ -536,8 +526,8 @@ Publishing, so no token is stored anywhere. PyPI never accepts the same version
|
|
|
536
526
|
|
|
537
527
|
Before tagging, manually check a first GPT-2 download, cancel and retry it, then run the example
|
|
538
528
|
on each supported compute backend. CPU and CUDA are covered by local development checks; MPS
|
|
539
|
-
still needs a check on Apple Silicon.
|
|
540
|
-
|
|
529
|
+
still needs a check on Apple Silicon. Pythia and Qwen 2.5 have also been run end to end with
|
|
530
|
+
their real weights; other families are checked when they load and in the tiny-model tests.
|
|
541
531
|
|
|
542
532
|
Model access goes through `logogram.backends.base.ModelBackend`. TransformerLens is the only
|
|
543
533
|
backend today; remote execution and other libraries can be added behind the same interface.
|
|
@@ -39,6 +39,12 @@ dependencies = [
|
|
|
39
39
|
"huggingface-hub>=0.23",
|
|
40
40
|
]
|
|
41
41
|
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/Jeevash23/logogram"
|
|
44
|
+
Source = "https://github.com/Jeevash23/logogram"
|
|
45
|
+
Issues = "https://github.com/Jeevash23/logogram/issues"
|
|
46
|
+
Changelog = "https://github.com/Jeevash23/logogram/blob/main/CHANGELOG.md"
|
|
47
|
+
|
|
42
48
|
[project.scripts]
|
|
43
49
|
logogram = "logogram.cli:main"
|
|
44
50
|
|
|
@@ -58,7 +64,7 @@ packages = ["src/logogram"]
|
|
|
58
64
|
artifacts = ["src/logogram/web_dist/**"]
|
|
59
65
|
|
|
60
66
|
[tool.hatch.build.targets.sdist]
|
|
61
|
-
include = ["src/logogram", "web/src", "web/public", "web/tests", "web/package.json", "web/package-lock.json", "web/index.html", "web/tsconfig.json", "web/vite.config.ts", "web/playwright.config.ts", "uv.lock", "tests", "scripts", "README.md", "LICENSE", "AGENTS.md"]
|
|
67
|
+
include = ["src/logogram", "web/src", "web/public", "web/tests", "web/package.json", "web/package-lock.json", "web/index.html", "web/tsconfig.json", "web/vite.config.ts", "web/playwright.config.ts", "uv.lock", "tests", "scripts", "README.md", "CHANGELOG.md", "LICENSE", "AGENTS.md"]
|
|
62
68
|
artifacts = ["src/logogram/web_dist/**"]
|
|
63
69
|
|
|
64
70
|
[tool.pytest.ini_options]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Logogram: a local-first workbench for causal experiments inside language models."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.1.
|
|
3
|
+
__version__ = "0.1.1"
|
|
4
4
|
# The release date of this version: after a few months the app suggests looking for a newer one
|
|
5
5
|
# (no network needed). Set it with every release.
|
|
6
6
|
__released__ = "2026-10-08"
|
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import torch
|
|
9
9
|
|
|
10
|
-
from logogram.backends.base import ModelBackend
|
|
10
|
+
from logogram.backends.base import ModelBackend, float64
|
|
11
11
|
from logogram.datasets import PromptRecord
|
|
12
12
|
from logogram.engine import compute_baselines
|
|
13
13
|
from logogram.prompts import PreparedPrompt, PromptIssue, group_by_length, prepare_prompt
|
|
@@ -153,7 +153,7 @@ def attention_report(
|
|
|
153
153
|
chosen = None
|
|
154
154
|
for start in range(0, len(same), batch_size):
|
|
155
155
|
pattern = backend.attention_pattern(tokens[start : start + batch_size], layer)[:, head]
|
|
156
|
-
pattern = pattern
|
|
156
|
+
pattern = float64(pattern).cpu()
|
|
157
157
|
total = pattern.sum(0) if total is None else total + pattern.sum(0)
|
|
158
158
|
for row, p in enumerate(same[start : start + batch_size]):
|
|
159
159
|
if p.index == index:
|
|
@@ -299,8 +299,8 @@ def sae_fit_report(
|
|
|
299
299
|
rows = torch.arange(len(idx))
|
|
300
300
|
plain = backend.final_logits(tokens)
|
|
301
301
|
edited = backend.edit_logits(tokens, sae.site, sae.layer, splice)
|
|
302
|
-
clean.append((plain[rows, answers] - plain[rows, distractors])
|
|
303
|
-
spliced.append((edited[rows, answers] - edited[rows, distractors])
|
|
302
|
+
clean.append(float64(plain[rows, answers] - plain[rows, distractors]))
|
|
303
|
+
spliced.append(float64(edited[rows, answers] - edited[rows, distractors]))
|
|
304
304
|
flat = torch.cat([a.reshape(-1, a.shape[-1]) for a in acts])
|
|
305
305
|
fit = fit_on(sae, flat)
|
|
306
306
|
ld, ld_spliced = torch.cat(clean), torch.cat(spliced)
|
|
@@ -388,7 +388,7 @@ def feature_report(
|
|
|
388
388
|
f, _ = sae.encode(
|
|
389
389
|
backend.capture(tokens[begin : begin + batch_size], [key])[key].float()
|
|
390
390
|
)
|
|
391
|
-
column = f[..., feature]
|
|
391
|
+
column = float64(f[..., feature]).cpu() # [B, pos]
|
|
392
392
|
for row, p in enumerate(idx):
|
|
393
393
|
prompt = prepared[p]
|
|
394
394
|
values = column[row]
|
|
@@ -20,7 +20,7 @@ from typing import Any
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
import torch
|
|
22
22
|
|
|
23
|
-
from logogram.backends.base import Cancelled, ModelBackend
|
|
23
|
+
from logogram.backends.base import Cancelled, ModelBackend, float64
|
|
24
24
|
from logogram.engine import (
|
|
25
25
|
EngineResult,
|
|
26
26
|
LayerFn,
|
|
@@ -82,8 +82,8 @@ def run_attribution_patching(
|
|
|
82
82
|
rows = torch.arange(len(idx))
|
|
83
83
|
for rs in sites:
|
|
84
84
|
key = (rs.kind, rs.layer)
|
|
85
|
-
diff = source_acts[key]
|
|
86
|
-
grad = grads[key]
|
|
85
|
+
diff = float64(source_acts[key]) - float64(receiver_acts[key])
|
|
86
|
+
grad = float64(grads[key])
|
|
87
87
|
if rs.kind == "head":
|
|
88
88
|
diff, grad = diff[:, :, rs.head], grad[:, :, rs.head]
|
|
89
89
|
per_position = (diff * grad).sum(-1) # [B, pos]
|
|
@@ -17,6 +17,17 @@ import torch
|
|
|
17
17
|
STREAM_KINDS = ("resid_pre", "resid_mid", "resid_post", "attn_out", "mlp_out")
|
|
18
18
|
ALL_KINDS = (*STREAM_KINDS, "head")
|
|
19
19
|
|
|
20
|
+
# Devices with no float64 arithmetic: Apple's Metal has no doubles.
|
|
21
|
+
NO_FLOAT64 = frozenset({"mps"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def float64(t: torch.Tensor) -> torch.Tensor:
|
|
25
|
+
"""``t`` in float64, for statistics and decompositions: on its own device, or on the CPU when
|
|
26
|
+
that device has no float64 (Apple's MPS), where converting it would fail. Every conversion to
|
|
27
|
+
float64 goes through here."""
|
|
28
|
+
device = torch.device("cpu") if t.device.type in NO_FLOAT64 else t.device
|
|
29
|
+
return t.to(device=device, dtype=torch.float64)
|
|
30
|
+
|
|
20
31
|
|
|
21
32
|
@dataclass(frozen=True)
|
|
22
33
|
class ModelInfo:
|
|
@@ -164,8 +175,10 @@ class ModelBackend(ABC):
|
|
|
164
175
|
|
|
165
176
|
Returns float64 tensors on the CPU, one row per prompt: ``embed`` ``[B]``, ``attn_out``
|
|
166
177
|
and ``mlp_out`` ``[B, layers]``, ``head`` ``[B, layers, heads]`` when ``heads`` is true,
|
|
167
|
-
``logit_diff`` ``[B]
|
|
168
|
-
minus every component's term)
|
|
178
|
+
``logit_diff`` ``[B]``, ``remainder`` ``[B]`` (what biases add: the logit difference
|
|
179
|
+
minus every component's term) and ``logit_scale`` ``[B]`` (the larger magnitude of the
|
|
180
|
+
answer's and the distractor's logit, which sets how finely the model's dtype resolves
|
|
181
|
+
them).
|
|
169
182
|
"""
|
|
170
183
|
raise BackendError("Direct logit attribution isn't supported by this model backend.")
|
|
171
184
|
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import fnmatch
|
|
11
11
|
import json
|
|
12
12
|
import logging
|
|
13
|
+
import math
|
|
13
14
|
import sys
|
|
14
15
|
import threading
|
|
15
16
|
from collections.abc import Callable
|
|
@@ -106,9 +107,15 @@ def resolve(model_id: str, revision: str | None) -> RepoFiles:
|
|
|
106
107
|
cached = cached_snapshot(model_id, revision)
|
|
107
108
|
if cached is not None:
|
|
108
109
|
sha, path = cached
|
|
109
|
-
files =
|
|
110
|
+
files = _select_files(
|
|
111
|
+
[(p.name, p.stat().st_size) for p in path.iterdir() if p.is_file()]
|
|
112
|
+
)
|
|
113
|
+
weights = [name for name, _ in files if name.endswith(".safetensors")]
|
|
110
114
|
return RepoFiles(
|
|
111
|
-
revision=sha,
|
|
115
|
+
revision=sha,
|
|
116
|
+
files=files,
|
|
117
|
+
n_params=count_stored_values(path, weights),
|
|
118
|
+
gated=False,
|
|
112
119
|
)
|
|
113
120
|
raise BackendError(
|
|
114
121
|
f"Couldn't reach Hugging Face, and {model_id} isn't in your local cache. "
|
|
@@ -127,15 +134,55 @@ def resolve(model_id: str, revision: str | None) -> RepoFiles:
|
|
|
127
134
|
)
|
|
128
135
|
|
|
129
136
|
|
|
137
|
+
def count_stored_values(folder: Path, names: list[str]) -> int | None:
|
|
138
|
+
"""How many values safetensors files hold, as the Hub reports for a model it knows, read from
|
|
139
|
+
their headers (no tensor is loaded). None if a file can't be read: loading it will say why."""
|
|
140
|
+
from safetensors import safe_open
|
|
141
|
+
|
|
142
|
+
total = 0
|
|
143
|
+
try:
|
|
144
|
+
for name in names:
|
|
145
|
+
with safe_open(str(folder / name), framework="pt", device="cpu") as f:
|
|
146
|
+
total += sum(math.prod(f.get_slice(key).get_shape()) for key in f.keys()) # noqa: SIM118
|
|
147
|
+
except Exception: # noqa: BLE001
|
|
148
|
+
return None
|
|
149
|
+
return total
|
|
150
|
+
|
|
151
|
+
|
|
130
152
|
def cached_snapshot(model_id: str, revision: str | None) -> tuple[str, Path] | None:
|
|
131
153
|
"""The (commit, folder) of a cached snapshot containing config.json, if any."""
|
|
132
|
-
|
|
154
|
+
found = cached_file(model_id, "config.json", revision)
|
|
155
|
+
return None if found is None else (found[0], found[1].parent)
|
|
133
156
|
|
|
134
|
-
|
|
157
|
+
|
|
158
|
+
def cached_file(repo_id: str, filename: str, revision: str | None) -> tuple[str, Path] | None:
|
|
159
|
+
"""The commit and local path of ``filename`` in the Hugging Face cache, at ``revision``.
|
|
160
|
+
|
|
161
|
+
Without a revision, the cached main branch; failing that, the only cached commit that has the
|
|
162
|
+
file. Logogram downloads exact commits, which records no branch in the cache, so this is how a
|
|
163
|
+
model it downloaded opens offline. Several commits and no branch would be a guess, so that is
|
|
164
|
+
refused with the commits to choose from.
|
|
165
|
+
"""
|
|
166
|
+
from huggingface_hub import constants, try_to_load_from_cache
|
|
167
|
+
from huggingface_hub.file_download import repo_folder_name
|
|
168
|
+
|
|
169
|
+
depth = len(Path(filename).parts)
|
|
170
|
+
found = try_to_load_from_cache(repo_id, filename, revision=revision or "main")
|
|
135
171
|
if isinstance(found, str):
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
172
|
+
return Path(found).parents[depth - 1].name, Path(found)
|
|
173
|
+
snapshots = (
|
|
174
|
+
Path(constants.HF_HUB_CACHE) / repo_folder_name(repo_id=repo_id, repo_type="model")
|
|
175
|
+
) / "snapshots"
|
|
176
|
+
if revision is not None or not snapshots.is_dir():
|
|
177
|
+
return None
|
|
178
|
+
commits = sorted(d for d in snapshots.iterdir() if (d / filename).is_file())
|
|
179
|
+
if len(commits) > 1:
|
|
180
|
+
names = ", ".join(d.name[:12] for d in commits)
|
|
181
|
+
raise BackendError(
|
|
182
|
+
f"Hugging Face can't be reached, and several revisions of {repo_id} are in your local "
|
|
183
|
+
f"cache ({names}). Enter the one to use as the revision."
|
|
184
|
+
)
|
|
185
|
+
return (commits[0].name, commits[0] / filename) if commits else None
|
|
139
186
|
|
|
140
187
|
|
|
141
188
|
def validate_local_weights(folder: Path) -> None:
|
|
@@ -238,9 +238,10 @@ def download_sae(
|
|
|
238
238
|
try:
|
|
239
239
|
info = HfApi().model_info(repo, revision=revision, files_metadata=True)
|
|
240
240
|
except Exception as exc: # noqa: BLE001
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
241
|
+
if hub._is_offline_error(exc):
|
|
242
|
+
cached = _cached(repo, prefix, revision)
|
|
243
|
+
if cached is not None:
|
|
244
|
+
return cached
|
|
244
245
|
raise hub._friendly_hub_error(repo, exc) from exc
|
|
245
246
|
sizes = {s.rfilename: int(s.size or 0) for s in info.siblings or []}
|
|
246
247
|
if f"{prefix}params.npz" in sizes:
|
|
@@ -265,13 +266,12 @@ def download_sae(
|
|
|
265
266
|
|
|
266
267
|
|
|
267
268
|
def _cached(repo: str, prefix: str, revision: str | None) -> tuple[str, Path] | None:
|
|
268
|
-
from
|
|
269
|
+
from logogram.backends.hub import cached_file
|
|
269
270
|
|
|
270
|
-
found =
|
|
271
|
-
if
|
|
271
|
+
found = cached_file(repo, f"{prefix}cfg.json", revision)
|
|
272
|
+
if found is None:
|
|
272
273
|
return None
|
|
273
|
-
|
|
274
|
-
if not any((
|
|
274
|
+
commit, path = found
|
|
275
|
+
if not any((path.parent / w).is_file() for w in WEIGHT_FILES):
|
|
275
276
|
return None
|
|
276
|
-
|
|
277
|
-
return snapshot.name, folder
|
|
277
|
+
return commit, path.parent
|