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.
Files changed (183) hide show
  1. logogram-0.1.1/CHANGELOG.md +60 -0
  2. logogram-0.1.0/README.md → logogram-0.1.1/PKG-INFO +75 -15
  3. logogram-0.1.0/PKG-INFO → logogram-0.1.1/README.md +38 -48
  4. {logogram-0.1.0 → logogram-0.1.1}/pyproject.toml +7 -1
  5. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/__init__.py +1 -1
  6. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/analysis.py +5 -5
  7. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/atp.py +3 -3
  8. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/base.py +15 -2
  9. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/hub.py +54 -7
  10. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/saes.py +10 -10
  11. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/transformer_lens.py +132 -26
  12. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/cli.py +21 -11
  13. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/direct.py +16 -6
  14. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/engine.py +18 -3
  15. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/features.py +8 -8
  16. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/app.py +5 -3
  17. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/models.py +1 -0
  18. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/steering.py +9 -6
  19. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/system.py +20 -7
  20. 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
  21. logogram-0.1.1/src/logogram/web_dist/assets/index-Dt6ecB66.css +1 -0
  22. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/web_dist/index.html +2 -2
  23. {logogram-0.1.0 → logogram-0.1.1}/tests/test_cli.py +33 -0
  24. logogram-0.1.1/tests/test_devices.py +50 -0
  25. {logogram-0.1.0 → logogram-0.1.1}/tests/test_direct.py +38 -0
  26. logogram-0.1.1/tests/test_loading.py +235 -0
  27. {logogram-0.1.0 → logogram-0.1.1}/tests/test_units.py +12 -0
  28. {logogram-0.1.0 → logogram-0.1.1}/web/src/api/client.ts +1 -1
  29. {logogram-0.1.0 → logogram-0.1.1}/web/src/api/types.ts +2 -0
  30. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelDialog.module.css +6 -0
  31. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelDialog.tsx +10 -3
  32. logogram-0.1.0/src/logogram/web_dist/assets/index-DTr8_ucV.css +0 -1
  33. {logogram-0.1.0 → logogram-0.1.1}/.gitignore +0 -0
  34. {logogram-0.1.0 → logogram-0.1.1}/AGENTS.md +0 -0
  35. {logogram-0.1.0 → logogram-0.1.1}/LICENSE +0 -0
  36. {logogram-0.1.0 → logogram-0.1.1}/scripts/check_privacy.py +0 -0
  37. {logogram-0.1.0 → logogram-0.1.1}/scripts/smoke_wheel.py +0 -0
  38. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/__main__.py +0 -0
  39. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/backends/__init__.py +0 -0
  40. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/compare.py +0 -0
  41. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/datasets.py +0 -0
  42. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/examples/ioi-gpt2/.gitignore +0 -0
  43. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/examples/ioi-gpt2/datasets/ioi.jsonl +0 -0
  44. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/examples/ioi-gpt2/experiments/ioi-head-patching/spec.json +0 -0
  45. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/examples/ioi-gpt2/project.json +0 -0
  46. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/exports.py +0 -0
  47. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/fileio.py +0 -0
  48. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/ioi.py +0 -0
  49. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/paths.py +0 -0
  50. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/project.py +0 -0
  51. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/prompts.py +0 -0
  52. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/research.py +0 -0
  53. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/results.py +0 -0
  54. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/runner.py +0 -0
  55. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/runs.py +0 -0
  56. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/sae.py +0 -0
  57. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/schema.py +0 -0
  58. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/__init__.py +0 -0
  59. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/security.py +0 -0
  60. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/server/state.py +0 -0
  61. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/sites.py +0 -0
  62. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/spec.py +0 -0
  63. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/stats.py +0 -0
  64. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/updates.py +0 -0
  65. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/verify.py +0 -0
  66. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/web_dist/assets/instrument-sans-latin-ext-standard-normal-C5E2Gvlv.woff2 +0 -0
  67. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/web_dist/assets/instrument-sans-latin-standard-normal-BVScPF0l.woff2 +0 -0
  68. {logogram-0.1.0 → logogram-0.1.1}/src/logogram/web_dist/favicon.svg +0 -0
  69. {logogram-0.1.0 → logogram-0.1.1}/tests/browser_server.py +0 -0
  70. {logogram-0.1.0 → logogram-0.1.1}/tests/conftest.py +0 -0
  71. {logogram-0.1.0 → logogram-0.1.1}/tests/test_architectures.py +0 -0
  72. {logogram-0.1.0 → logogram-0.1.1}/tests/test_atp.py +0 -0
  73. {logogram-0.1.0 → logogram-0.1.1}/tests/test_engine.py +0 -0
  74. {logogram-0.1.0 → logogram-0.1.1}/tests/test_explorer.py +0 -0
  75. {logogram-0.1.0 → logogram-0.1.1}/tests/test_hardening.py +0 -0
  76. {logogram-0.1.0 → logogram-0.1.1}/tests/test_paths.py +0 -0
  77. {logogram-0.1.0 → logogram-0.1.1}/tests/test_privacy.py +0 -0
  78. {logogram-0.1.0 → logogram-0.1.1}/tests/test_release_fixes.py +0 -0
  79. {logogram-0.1.0 → logogram-0.1.1}/tests/test_sae.py +0 -0
  80. {logogram-0.1.0 → logogram-0.1.1}/tests/test_sanity.py +0 -0
  81. {logogram-0.1.0 → logogram-0.1.1}/tests/test_server.py +0 -0
  82. {logogram-0.1.0 → logogram-0.1.1}/tests/test_steering.py +0 -0
  83. {logogram-0.1.0 → logogram-0.1.1}/tests/test_updates.py +0 -0
  84. {logogram-0.1.0 → logogram-0.1.1}/uv.lock +0 -0
  85. {logogram-0.1.0 → logogram-0.1.1}/web/index.html +0 -0
  86. {logogram-0.1.0 → logogram-0.1.1}/web/package-lock.json +0 -0
  87. {logogram-0.1.0 → logogram-0.1.1}/web/package.json +0 -0
  88. {logogram-0.1.0 → logogram-0.1.1}/web/playwright.config.ts +0 -0
  89. {logogram-0.1.0 → logogram-0.1.1}/web/public/favicon.svg +0 -0
  90. {logogram-0.1.0 → logogram-0.1.1}/web/src/App.module.css +0 -0
  91. {logogram-0.1.0 → logogram-0.1.1}/web/src/App.tsx +0 -0
  92. {logogram-0.1.0 → logogram-0.1.1}/web/src/api/events.ts +0 -0
  93. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/CommandPalette.module.css +0 -0
  94. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/CommandPalette.tsx +0 -0
  95. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/CopyCommand.module.css +0 -0
  96. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/CopyCommand.tsx +0 -0
  97. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Distribution.module.css +0 -0
  98. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Distribution.tsx +0 -0
  99. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/FolderPicker.module.css +0 -0
  100. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/FolderPicker.tsx +0 -0
  101. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Header.module.css +0 -0
  102. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Header.tsx +0 -0
  103. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Heatmap/Heatmap.module.css +0 -0
  104. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Heatmap/Heatmap.tsx +0 -0
  105. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Heatmap/ScaleBar.tsx +0 -0
  106. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/History.module.css +0 -0
  107. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/History.tsx +0 -0
  108. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Inspector.module.css +0 -0
  109. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Inspector.tsx +0 -0
  110. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Logogram.tsx +0 -0
  111. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/LogogramDial.module.css +0 -0
  112. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/LogogramDial.tsx +0 -0
  113. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelMap/Legend.tsx +0 -0
  114. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelMap/ModelMap.module.css +0 -0
  115. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ModelMap/ModelMap.tsx +0 -0
  116. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Notices.module.css +0 -0
  117. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Notices.tsx +0 -0
  118. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/RobustnessDialog.tsx +0 -0
  119. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Splitter.module.css +0 -0
  120. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/Splitter.tsx +0 -0
  121. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/StatusLine.module.css +0 -0
  122. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/StatusLine.tsx +0 -0
  123. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/TokenStrip.module.css +0 -0
  124. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/TokenStrip.tsx +0 -0
  125. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/TopBar.module.css +0 -0
  126. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/TopBar.tsx +0 -0
  127. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/UpdateNotice.module.css +0 -0
  128. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/UpdateNotice.tsx +0 -0
  129. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ui/Icon.tsx +0 -0
  130. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ui/index.tsx +0 -0
  131. {logogram-0.1.0 → logogram-0.1.1}/web/src/components/ui/ui.module.css +0 -0
  132. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/analysis.ts +0 -0
  133. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/canvas.ts +0 -0
  134. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/color.ts +0 -0
  135. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/export.ts +0 -0
  136. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/format.ts +0 -0
  137. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/heatmapNavigation.ts +0 -0
  138. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/hooks.ts +0 -0
  139. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/keys.ts +0 -0
  140. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/logogram.ts +0 -0
  141. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/sites.ts +0 -0
  142. {logogram-0.1.0 → logogram-0.1.1}/web/src/lib/spec.ts +0 -0
  143. {logogram-0.1.0 → logogram-0.1.1}/web/src/main.tsx +0 -0
  144. {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Projects.tsx +0 -0
  145. {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Screens.module.css +0 -0
  146. {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/SystemCheck.tsx +0 -0
  147. {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Welcome.tsx +0 -0
  148. {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Workbench.module.css +0 -0
  149. {logogram-0.1.0 → logogram-0.1.1}/web/src/screens/Workbench.tsx +0 -0
  150. {logogram-0.1.0 → logogram-0.1.1}/web/src/store/app.ts +0 -0
  151. {logogram-0.1.0 → logogram-0.1.1}/web/src/styles/global.css +0 -0
  152. {logogram-0.1.0 → logogram-0.1.1}/web/src/styles/tokens.css +0 -0
  153. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/AttentionView.module.css +0 -0
  154. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/AttentionView.tsx +0 -0
  155. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/BaselineView.module.css +0 -0
  156. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/BaselineView.tsx +0 -0
  157. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/CompareView.module.css +0 -0
  158. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/CompareView.tsx +0 -0
  159. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ExperimentView.module.css +0 -0
  160. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ExperimentView.tsx +0 -0
  161. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ExploreView.module.css +0 -0
  162. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ExploreView.tsx +0 -0
  163. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/FeaturesView.module.css +0 -0
  164. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/FeaturesView.tsx +0 -0
  165. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/Forest.module.css +0 -0
  166. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/Forest.tsx +0 -0
  167. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/HeadComparisonView.module.css +0 -0
  168. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/HeadComparisonView.tsx +0 -0
  169. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/PredictionsView.module.css +0 -0
  170. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/PredictionsView.tsx +0 -0
  171. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/PromptsView.tsx +0 -0
  172. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ResearchView.module.css +0 -0
  173. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ResearchView.tsx +0 -0
  174. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ResultsView.module.css +0 -0
  175. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/ResultsView.tsx +0 -0
  176. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/SpecView.module.css +0 -0
  177. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/SpecView.tsx +0 -0
  178. {logogram-0.1.0 → logogram-0.1.1}/web/src/views/views.module.css +0 -0
  179. {logogram-0.1.0 → logogram-0.1.1}/web/tests/context.unit.ts +0 -0
  180. {logogram-0.1.0 → logogram-0.1.1}/web/tests/logogram.unit.ts +0 -0
  181. {logogram-0.1.0 → logogram-0.1.1}/web/tests/workbench.browser.ts +0 -0
  182. {logogram-0.1.0 → logogram-0.1.1}/web/tsconfig.json +0 -0
  183. {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
- Until Logogram is published on PyPI, install it from a clone of this repository with
29
- `uv tool install .` instead. The web app is prebuilt inside the package, so you never need Node.
30
- The commands in the table below work the same way with `.` in place of `logogram`.
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; zero ablation of head outputs does, because value biases are folded. |
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, and the one run end to end with real
452
- weights. The test suite runs the sanity checks on tiny random models of the Llama, Pythia, Qwen 2,
453
- Gemma 2 and OLMo 2 families without downloading anything. Pythia publishes checkpoints from
454
- throughout training as revisions (`step1000` to `step143000`), so an experiment can be rerun at
455
- several points of training.
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. Other model families are checked when they load and in the
507
- tiny-model tests, but not yet run end to end with their real weights.
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
- Until Logogram is published on PyPI, install it from a clone of this repository with
62
- `uv tool install .` instead. The web app is prebuilt inside the package, so you never need Node.
63
- The commands in the table below work the same way with `.` in place of `logogram`.
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; zero ablation of head outputs does, because value biases are folded. |
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, and the one run end to end with real
485
- weights. The test suite runs the sanity checks on tiny random models of the Llama, Pythia, Qwen 2,
486
- Gemma 2 and OLMo 2 families without downloading anything. Pythia publishes checkpoints from
487
- throughout training as revisions (`step1000` to `step143000`), so an experiment can be rerun at
488
- several points of training.
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. Other model families are checked when they load and in the
540
- tiny-model tests, but not yet run end to end with their real weights.
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.0"
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.double().cpu()
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]).double())
303
- spliced.append((edited[rows, answers] - edited[rows, distractors]).double())
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].double().cpu() # [B, pos]
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].double() - receiver_acts[key].double()
86
- grad = grads[key].double()
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]`` and ``remainder`` ``[B]`` (what biases add: the logit difference
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 = [(p.name, p.stat().st_size) for p in path.iterdir() if p.is_file()]
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, files=_select_files(files), n_params=None, gated=False
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
- from huggingface_hub import try_to_load_from_cache
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
- found = try_to_load_from_cache(model_id, "config.json", revision=revision or "main")
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
- path = Path(found).parent
137
- return path.name, path
138
- return None
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
- cached = _cached(repo, prefix, revision)
242
- if cached is not None and hub._is_offline_error(exc):
243
- return cached
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 huggingface_hub import try_to_load_from_cache
269
+ from logogram.backends.hub import cached_file
269
270
 
270
- found = try_to_load_from_cache(repo, f"{prefix}cfg.json", revision=revision or "main")
271
- if not isinstance(found, str):
271
+ found = cached_file(repo, f"{prefix}cfg.json", revision)
272
+ if found is None:
272
273
  return None
273
- folder = Path(found).parent
274
- if not any((folder / w).is_file() for w in WEIGHT_FILES):
274
+ commit, path = found
275
+ if not any((path.parent / w).is_file() for w in WEIGHT_FILES):
275
276
  return None
276
- snapshot = folder if not prefix else Path(found).parents[prefix.count("/")]
277
- return snapshot.name, folder
277
+ return commit, path.parent