attnread 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attnread/__init__.py +4 -0
- attnread/_core.py +215 -0
- attnread/py.typed +0 -0
- attnread-0.1.0.dist-info/METADATA +162 -0
- attnread-0.1.0.dist-info/RECORD +8 -0
- attnread-0.1.0.dist-info/WHEEL +5 -0
- attnread-0.1.0.dist-info/licenses/LICENSE +21 -0
- attnread-0.1.0.dist-info/top_level.txt +1 -0
attnread/__init__.py
ADDED
attnread/_core.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Local attention recovery through NNsight partial-identity value probes."""
|
|
2
|
+
import inspect
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import torch
|
|
8
|
+
from nnsight import NNsight
|
|
9
|
+
|
|
10
|
+
Head = tuple[str, int, int]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class _Spec:
|
|
15
|
+
path: str
|
|
16
|
+
value: str
|
|
17
|
+
output: str
|
|
18
|
+
heads: int
|
|
19
|
+
kv_heads: int
|
|
20
|
+
dim: int
|
|
21
|
+
fused: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _spec(path, module):
|
|
25
|
+
if getattr(module, 'is_cross_attention', False) or any(
|
|
26
|
+
part in path.split('.') for part in ('cross_attn', 'encoder_attn', 'crossattention')
|
|
27
|
+
):
|
|
28
|
+
return None
|
|
29
|
+
dim = getattr(module, 'head_dim', None)
|
|
30
|
+
if not isinstance(dim, int) or dim <= 0:
|
|
31
|
+
return None
|
|
32
|
+
for output in ('o_proj', 'out_proj'):
|
|
33
|
+
v = getattr(module, 'v_proj', None)
|
|
34
|
+
o = getattr(module, output, None)
|
|
35
|
+
if all(hasattr(module, name) for name in ('q_proj', 'k_proj')) and v is not None and o is not None:
|
|
36
|
+
n, kv = getattr(o, 'in_features', 0), getattr(v, 'out_features', 0)
|
|
37
|
+
if n > 0 and kv > 0 and n % dim == kv % dim == 0:
|
|
38
|
+
return _Spec(path, 'v_proj', output, n // dim, kv // dim, dim)
|
|
39
|
+
# Fused QKV layouts are explicit: Q, then K, then V, with equal head counts.
|
|
40
|
+
if type(module).__name__ == 'GPT2Attention' and module.c_attn.nf == 3 * module.num_heads * dim:
|
|
41
|
+
return _Spec(path, 'c_attn', 'c_proj', module.num_heads, module.num_heads, dim, True)
|
|
42
|
+
if type(module).__name__ == 'Qwen3VLVisionAttention':
|
|
43
|
+
return _Spec(path, 'qkv', 'proj', module.num_heads, module.num_heads, dim, True)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _discover(model, attention_modules=None):
|
|
48
|
+
groups = {}
|
|
49
|
+
if attention_modules is not None:
|
|
50
|
+
for stream, paths in attention_modules.items():
|
|
51
|
+
groups[stream] = []
|
|
52
|
+
for path in paths:
|
|
53
|
+
try:
|
|
54
|
+
spec = _spec(path, model.get_submodule(path))
|
|
55
|
+
except AttributeError:
|
|
56
|
+
spec = None
|
|
57
|
+
if spec is None:
|
|
58
|
+
raise ValueError(f'No supported attention projection layout at {path!r}')
|
|
59
|
+
groups[stream].append(spec)
|
|
60
|
+
else:
|
|
61
|
+
for path, module in model.named_modules():
|
|
62
|
+
spec = _spec(path, module)
|
|
63
|
+
if spec is not None:
|
|
64
|
+
stream = 'vision' if any(part == 'visual' or part.startswith('vision')
|
|
65
|
+
for part in path.split('.')) else 'text'
|
|
66
|
+
groups.setdefault(stream, []).append(spec)
|
|
67
|
+
if not any(groups.values()):
|
|
68
|
+
raise ValueError('No supported attention projection layout found; see package compatibility')
|
|
69
|
+
return groups
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _validate_inputs(model, inputs):
|
|
73
|
+
if model.training or any(module.training for module in model.modules()):
|
|
74
|
+
raise ValueError('Use evaluation mode: call model.eval()')
|
|
75
|
+
if inputs.get('past_key_values') is not None:
|
|
76
|
+
raise ValueError('Use an uncached full prefix; past_key_values is unsupported')
|
|
77
|
+
config = getattr(model, 'config', None)
|
|
78
|
+
configs = [config, getattr(config, 'text_config', None), getattr(config, 'vision_config', None)]
|
|
79
|
+
if inputs.get('output_attentions') or any(getattr(c, 'output_attentions', False) for c in configs):
|
|
80
|
+
raise ValueError('output_attentions=True can switch attention backends; disable it')
|
|
81
|
+
ids, embeds = inputs.get('input_ids'), inputs.get('inputs_embeds')
|
|
82
|
+
if not ((isinstance(ids, torch.Tensor) and ids.ndim == 2 and min(ids.shape) > 0) or
|
|
83
|
+
(isinstance(embeds, torch.Tensor) and embeds.ndim == 3 and min(embeds.shape) > 0)):
|
|
84
|
+
raise ValueError('Use nonempty input_ids [batch, tokens] or inputs_embeds [batch, tokens, dim]')
|
|
85
|
+
if hasattr(model, '_orig_mod'):
|
|
86
|
+
raise ValueError('Compiled models are outside the supported scope')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _envoy(wrapped, path):
|
|
90
|
+
node = wrapped
|
|
91
|
+
for part in path.split('.'):
|
|
92
|
+
node = node[int(part)] if part.isdigit() else getattr(node, part)
|
|
93
|
+
return node
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def extract_attention(
|
|
97
|
+
model: torch.nn.Module,
|
|
98
|
+
inputs: Mapping[str, Any],
|
|
99
|
+
heads: Sequence[tuple[int, int] | Head] | None = None,
|
|
100
|
+
*,
|
|
101
|
+
attention_modules: Mapping[str, Sequence[str]] | None = None,
|
|
102
|
+
) -> tuple[dict[Head, torch.Tensor], dict[str, Any]]:
|
|
103
|
+
"""Recover requested heads; return CPU [B,T,T] tensors and preservation checks.
|
|
104
|
+
|
|
105
|
+
(L,H) addresses text heads; (stream,L,H) also addresses vision/custom streams.
|
|
106
|
+
None selects every text head. Ordinary HF evaluation models only, uncached.
|
|
107
|
+
Partial-identity V probes preserve Q/K, masks, backend and downstream values.
|
|
108
|
+
Vision's packed tokens have a singleton batch axis. Reconstruction is finite
|
|
109
|
+
precision and quadratic in token count; head selection limits saved memory.
|
|
110
|
+
"""
|
|
111
|
+
_validate_inputs(model, inputs)
|
|
112
|
+
specs = _discover(model, attention_modules)
|
|
113
|
+
if heads is None:
|
|
114
|
+
heads = [('text', layer, head) for layer, spec in enumerate(specs.get('text', []))
|
|
115
|
+
for head in range(spec.heads)]
|
|
116
|
+
selected, grouped = [], {}
|
|
117
|
+
for address in heads:
|
|
118
|
+
if not isinstance(address, (tuple, list)) or len(address) not in (2, 3):
|
|
119
|
+
raise ValueError('heads must contain (layer, head) or (stream, layer, head) addresses')
|
|
120
|
+
stream, layer, head = ('text', *address) if len(address) == 2 else address
|
|
121
|
+
if (not isinstance(stream, str) or type(layer) is not int or type(head) is not int or
|
|
122
|
+
stream not in specs or not 0 <= layer < len(specs[stream]) or
|
|
123
|
+
not 0 <= head < specs[stream][layer].heads):
|
|
124
|
+
raise ValueError(f'Invalid heads address: {address!r}')
|
|
125
|
+
key = (stream, layer, head)
|
|
126
|
+
if key in selected:
|
|
127
|
+
raise ValueError(f'Duplicate heads address: {key!r}')
|
|
128
|
+
selected.append(key)
|
|
129
|
+
grouped.setdefault((stream, layer), []).append(head)
|
|
130
|
+
if not selected:
|
|
131
|
+
raise ValueError('heads selection is empty or no text attention was discovered')
|
|
132
|
+
paths = [specs[stream][layer].path for stream, layer in grouped]
|
|
133
|
+
if len(set(paths)) != len(paths):
|
|
134
|
+
raise ValueError('Do not address the same module through multiple stream aliases')
|
|
135
|
+
kwargs = {**inputs, 'use_cache': False, 'return_dict': True}
|
|
136
|
+
if 'logits_to_keep' in inspect.signature(type(model).forward).parameters:
|
|
137
|
+
kwargs['logits_to_keep'] = 1
|
|
138
|
+
checks = {'baseline_logit_max_abs': 0.0, 'probe_logit_max_abs': 0.0,
|
|
139
|
+
'unpatched_logit_max_abs': 0.0, 'heads': selected, 'probes': 0,
|
|
140
|
+
'modules': {stream: [{'path': s.path, 'heads': s.heads, 'head_dim': s.dim,
|
|
141
|
+
'packed': None} for s in group]
|
|
142
|
+
for stream, group in specs.items()}}
|
|
143
|
+
patterns = {}
|
|
144
|
+
with torch.no_grad():
|
|
145
|
+
original = model(**kwargs).logits.detach().clone()
|
|
146
|
+
wrapped = NNsight(model)
|
|
147
|
+
for (stream, layer), chosen in grouped.items():
|
|
148
|
+
spec = specs[stream][layer]
|
|
149
|
+
attn = _envoy(wrapped, spec.path)
|
|
150
|
+
value, output = getattr(attn, spec.value), getattr(attn, spec.output)
|
|
151
|
+
# Save the genuine result first, for restoration before the output projection.
|
|
152
|
+
with wrapped.trace(**kwargs):
|
|
153
|
+
saved_z = output.input.detach().clone().save()
|
|
154
|
+
saved_logits = wrapped.output.logits.detach().clone().save()
|
|
155
|
+
baseline_z = saved_z
|
|
156
|
+
packed = baseline_z.ndim == 2
|
|
157
|
+
if baseline_z.ndim not in (2, 3) or baseline_z.shape[-1] != spec.heads * spec.dim:
|
|
158
|
+
raise ValueError(f'Unsupported merged-head output layout at {spec.path}')
|
|
159
|
+
batch, length = (1, baseline_z.shape[0]) if packed else baseline_z.shape[:2]
|
|
160
|
+
checks['modules'][stream][layer].update(packed=packed, tokens=length, batch=batch)
|
|
161
|
+
checks['baseline_logit_max_abs'] = max(checks['baseline_logit_max_abs'],
|
|
162
|
+
(saved_logits-original).abs().max().item())
|
|
163
|
+
pieces = []
|
|
164
|
+
for offset in range(0, length, spec.dim):
|
|
165
|
+
width = min(spec.dim, length-offset)
|
|
166
|
+
basis = torch.zeros(length, spec.dim, device=baseline_z.device, dtype=baseline_z.dtype)
|
|
167
|
+
basis[offset:offset+width, :width] = torch.eye(width, device=basis.device, dtype=basis.dtype)
|
|
168
|
+
fake_v = basis[:, None, :].expand(length, spec.kv_heads, spec.dim).reshape(length, -1)
|
|
169
|
+
if not packed:
|
|
170
|
+
fake_v = fake_v[None].expand(batch, -1, -1).contiguous()
|
|
171
|
+
with wrapped.trace(**kwargs):
|
|
172
|
+
if spec.fused:
|
|
173
|
+
# Preserve the original Q and K portions of a fused projection.
|
|
174
|
+
value.output = torch.cat([value.output[..., :2*spec.heads*spec.dim], fake_v], dim=-1)
|
|
175
|
+
else:
|
|
176
|
+
value.output = fake_v
|
|
177
|
+
# Slice heads before saving, so unrequested patterns are never transferred.
|
|
178
|
+
block = output.input.reshape(batch, length, spec.heads, spec.dim).permute(
|
|
179
|
+
0, 2, 1, 3)[:, chosen, :, :width].detach().clone().save()
|
|
180
|
+
output.input = baseline_z
|
|
181
|
+
probe_logits = wrapped.output.logits.detach().clone().save()
|
|
182
|
+
pieces.append(block.cpu())
|
|
183
|
+
checks['probe_logit_max_abs'] = max(checks['probe_logit_max_abs'],
|
|
184
|
+
(probe_logits-original).abs().max().item())
|
|
185
|
+
checks['probes'] += 1
|
|
186
|
+
weights = torch.cat(pieces, dim=-1)
|
|
187
|
+
for index, head in enumerate(chosen):
|
|
188
|
+
patterns[(stream, layer, head)] = weights[:, index]
|
|
189
|
+
untouched = model(**kwargs).logits
|
|
190
|
+
checks['unpatched_logit_max_abs'] = (untouched-original).abs().max().item()
|
|
191
|
+
return {key: patterns[key] for key in selected}, checks
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def reconstruct_attention(model, inputs, layers=None, *, heads=None, attention_modules=None):
|
|
195
|
+
"""Research compatibility adapter: return stacked text layers and checks."""
|
|
196
|
+
_validate_inputs(model, inputs)
|
|
197
|
+
if attention_modules is not None and layers is not None:
|
|
198
|
+
raise ValueError('Choose layers or attention_modules, not both')
|
|
199
|
+
explicit = {'text': list(attention_modules)} if attention_modules is not None else None
|
|
200
|
+
specs = _discover(model, explicit).get('text', [])
|
|
201
|
+
layers = list(range(len(specs))) if layers is None else list(layers)
|
|
202
|
+
if not layers or any(type(i) is not int or not 0 <= i < len(specs) for i in layers) or len(set(layers)) != len(layers):
|
|
203
|
+
raise ValueError('layers must contain distinct integer text indices in range')
|
|
204
|
+
if heads is None:
|
|
205
|
+
chosen = list(range(specs[layers[0]].heads))
|
|
206
|
+
else:
|
|
207
|
+
chosen = [heads] if type(heads) is int else list(heads)
|
|
208
|
+
if not chosen or any(type(h) is not int or not 0 <= h < specs[i].heads for i in layers for h in chosen) or len(set(chosen)) != len(chosen):
|
|
209
|
+
raise ValueError('heads must contain distinct integer head indices in range')
|
|
210
|
+
patterns, checks = extract_attention(model, inputs,
|
|
211
|
+
heads=[(i, h) for i in layers for h in chosen], attention_modules=explicit)
|
|
212
|
+
weights = torch.stack([torch.stack([patterns[('text', i, h)] for h in chosen], dim=1)
|
|
213
|
+
for i in layers], dim=1)
|
|
214
|
+
checks.update(layers=layers, heads=chosen, modules=[specs[i].path for i in layers])
|
|
215
|
+
return weights, checks
|
attnread/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: attnread
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Recover attention weights without changing the attention backend.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Science/Research
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: nnsight==0.7.0
|
|
15
|
+
Requires-Dist: torch<2.15,>=2.14
|
|
16
|
+
Requires-Dist: transformers<5.20,>=5.19
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest<10,>=9; extra == "dev"
|
|
19
|
+
Requires-Dist: build<2,>=1.2; extra == "dev"
|
|
20
|
+
Requires-Dist: twine<7,>=6; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# AttnRead
|
|
24
|
+
|
|
25
|
+
**Recover attention weights without changing the attention backend.**
|
|
26
|
+
|
|
27
|
+
AttnRead is a lightweight Python toolkit for extracting attention patterns from optimized transformer attention implementations, including PyTorch SDPA and FlashAttention 2.
|
|
28
|
+
|
|
29
|
+
Using identity-value passes and activation restoration, AttnRead recovers attention patterns while preserving the model's downstream computation. No fallback to eager attention required.
|
|
30
|
+
|
|
31
|
+
Version **0.1.0**, distributed under the MIT License. The distribution and
|
|
32
|
+
Python import name are both `attnread`.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
Use Python 3.11 or newer in a venv. Install your preferred CPU/CUDA PyTorch
|
|
37
|
+
build first, then install AttnRead:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python -m pip install attnread==0.1.0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import torch
|
|
47
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
48
|
+
from attnread import extract_attention
|
|
49
|
+
|
|
50
|
+
checkpoint = "Qwen/Qwen3-4B"
|
|
51
|
+
revision = "1cfa9a7208912126459214e8b04321603b3df60c"
|
|
52
|
+
model = AutoModelForCausalLM.from_pretrained(
|
|
53
|
+
checkpoint, revision=revision, dtype=torch.bfloat16,
|
|
54
|
+
attn_implementation="sdpa",
|
|
55
|
+
).to("cuda").eval().requires_grad_(False)
|
|
56
|
+
tokenizer = AutoTokenizer.from_pretrained(checkpoint, revision=revision)
|
|
57
|
+
inputs = tokenizer("Describe the colors of an autumn leaf.",
|
|
58
|
+
return_tensors="pt").to(model.device)
|
|
59
|
+
|
|
60
|
+
patterns, checks = extract_attention(model, inputs, heads=[(0, 3), (12, 7)])
|
|
61
|
+
head = patterns[("text", 0, 3)] # CPU tensor: [batch, query tokens, key tokens].
|
|
62
|
+
assert checks["probe_logit_max_abs"] == 0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use the same call with `AutoModelForImageTextToText` and processor outputs:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# (layer, head) means text attention; qualify a vision head with its stream.
|
|
69
|
+
patterns, checks = extract_attention(
|
|
70
|
+
vlm, processed_inputs, heads=[(12, 3), ("vision", 4, 2)],
|
|
71
|
+
)
|
|
72
|
+
text = patterns[("text", 12, 3)]
|
|
73
|
+
vision = patterns[("vision", 4, 2)]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Selection.** `heads=None` extracts every discovered text head. A sequence of
|
|
77
|
+
`(layer, head)` or `(stream, layer, head)` addresses extracts only those heads;
|
|
78
|
+
returned keys always use the full triple, in request order. Layers and heads are
|
|
79
|
+
zero-based within each stream, following discovered module order (reported in
|
|
80
|
+
`checks["modules"]`). Use explicit paths for sparse/hybrid stacks or unusual
|
|
81
|
+
module naming. Different streams can have different sequence lengths, so results
|
|
82
|
+
are a dictionary. Probes are grouped by layer and save/transfer only selected
|
|
83
|
+
heads. The underlying model still computes all heads; head selection reduces
|
|
84
|
+
saved memory, not the number of identity probes. Start with one short prefix and
|
|
85
|
+
one head. Unknown, duplicate or out-of-range addresses raise `ValueError`.
|
|
86
|
+
|
|
87
|
+
**Model interface.** Pass an ordinary HF model in evaluation mode and its
|
|
88
|
+
preprocessed tensor input mapping. Discovery recognizes separate `q_proj`,
|
|
89
|
+
`k_proj`, `v_proj` with `o_proj`/`out_proj`, GPT-2's fused `c_attn`/`c_proj`, and
|
|
90
|
+
Qwen3-VL vision `qkv`/`proj`. Vision module paths identify the `vision` stream;
|
|
91
|
+
others belong to `text`. For unusual or ambiguous naming, supply explicit paths:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
patterns, checks = extract_attention(model, inputs, heads=[("text", 0, 1)],
|
|
95
|
+
attention_modules={"text": ["model.layers.7.self_attn"]})
|
|
96
|
+
# With explicit lists, layer 0 denotes the first entry in that list.
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This is structural compatibility, not a promise that every HF architecture works.
|
|
100
|
+
The supported operation must be linear in V, with attention weights independent
|
|
101
|
+
of V, and a conventional merged-head input to the output projection. Tested
|
|
102
|
+
families include Qwen3, Llama, Mistral (sliding window), Gemma2 (score softcapping),
|
|
103
|
+
GPT-2, LLaVA/CLIP vision with pixels, and Qwen3-VL packed vision/text. Unsupported
|
|
104
|
+
projection layouts fail explicitly. Cross-attention, nonlinear/value-dependent
|
|
105
|
+
attention, quantized/compiled models, distributed/sharded execution and remote
|
|
106
|
+
NNsight are outside the verified scope. No weights or inputs are downloaded by
|
|
107
|
+
the extraction call. Model weights stay unchanged; each probe restores the genuine
|
|
108
|
+
pre-output-projection activation before the rest of the forward runs.
|
|
109
|
+
|
|
110
|
+
**Inputs and results.** Use nonempty `input_ids` or `inputs_embeds`, no KV cache,
|
|
111
|
+
and `output_attentions=False`. Sequence length comes from the actual attention
|
|
112
|
+
activation, accounting for expanded image tokens. Parameter gradients should be
|
|
113
|
+
disabled for inference to reduce NNsight memory. Do not run concurrent calls on
|
|
114
|
+
the same model. Qwen-VL vision packs patches from all images/frames on one axis;
|
|
115
|
+
its result is `[1, packed_tokens, packed_tokens]`, with separate images/frames
|
|
116
|
+
forming disconnected blocks. Interpret coordinates with the processor's grids;
|
|
117
|
+
these are patch tokens, not merged decoder image tokens.
|
|
118
|
+
|
|
119
|
+
Checks contain module paths/layouts, selected addresses, probe count, and maximum
|
|
120
|
+
absolute logit deltas for traced baselines, probes and a final unpatched pass.
|
|
121
|
+
Inspect the deltas: nondeterministic execution may produce nonzero values even
|
|
122
|
+
without a probe. Recovered weights are finite-precision measurements and are not
|
|
123
|
+
renormalized or guaranteed bit-exact native softmax probabilities. Masks and
|
|
124
|
+
Q/K-only score modifications stay inside the original attention implementation.
|
|
125
|
+
|
|
126
|
+
**Backend.** `sdpa` uses normal PyTorch backend selection. Force math by wrapping
|
|
127
|
+
extraction in `torch.nn.attention.sdpa_kernel(SDPBackend.MATH)`. Package FA2 requires
|
|
128
|
+
a separately installed compatible `flash-attn` and `attn_implementation="flash_attention_2"`.
|
|
129
|
+
The library does not install FA2 or override precision/backend.
|
|
130
|
+
|
|
131
|
+
**Cost.** For each selected layer with prefix T and head dimension d, reconstruction
|
|
132
|
+
needs 1 + ceil(T/d) full forwards, plus two global unpatched forwards. Selected-head
|
|
133
|
+
CPU storage scales as B×selected_heads×T² per layer; this remains quadratic.
|
|
134
|
+
NNsight brings dependencies, but the package itself contains only the extractor.
|
|
135
|
+
|
|
136
|
+
**Compatibility.** Initially constrained to NNsight 0.7.0, Transformers 5.19.x
|
|
137
|
+
and PyTorch 2.14.x; tested on Python 3.11. Experiment pins live in the research repository’s `requirements.txt`;
|
|
138
|
+
the package has no CUDA URLs or plotting dependencies.
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
From the standalone source directory:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m venv .venv
|
|
146
|
+
source .venv/bin/activate
|
|
147
|
+
# Install your preferred CPU/CUDA PyTorch build first.
|
|
148
|
+
python -m pip install -e '.[dev]'
|
|
149
|
+
python -m pytest -q test_attnread.py
|
|
150
|
+
python -m build
|
|
151
|
+
python -m twine check --strict dist/*
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
On Oscar, run installs, builds and tests in a CPU Slurm allocation; pretrained
|
|
155
|
+
GPU examples require a GPU allocation. The source checkout includes the release
|
|
156
|
+
guide and review notebook; the notebook uses in-memory example inputs and is
|
|
157
|
+
excluded from release archives. No publishing step runs automatically.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
MIT License. Copyright (c) 2026 Etha. The full license text is included in the
|
|
162
|
+
source archive and wheel.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
attnread/__init__.py,sha256=q7PDqt5zmBTn2vhP0GXJ4TTVNmcYMROENKlFtASi-kI,142
|
|
2
|
+
attnread/_core.py,sha256=4GIxsbqapW7_drM3bBDOK2a7sW8vkHyIQQ8f03j8CaY,11438
|
|
3
|
+
attnread/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
attnread-0.1.0.dist-info/licenses/LICENSE,sha256=Xi4piPMZc-0ukHN4eM_sq8DGZcEfzPRr8Y2cIo1d8pM,1061
|
|
5
|
+
attnread-0.1.0.dist-info/METADATA,sha256=D48LqpRQnnelzX0LPbDMYhVpb82xQyg0RgILX3sQmk4,7581
|
|
6
|
+
attnread-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
attnread-0.1.0.dist-info/top_level.txt,sha256=0WrqukxxD6qUq7m_z28avBhVrgWOdeP7lQOFI7sq7yk,9
|
|
8
|
+
attnread-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Etha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
attnread
|