bergson 0.0.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.
- bergson-0.0.1/LICENSE +21 -0
- bergson-0.0.1/PKG-INFO +158 -0
- bergson-0.0.1/README.md +132 -0
- bergson-0.0.1/bergson/__init__.py +21 -0
- bergson-0.0.1/bergson/__main__.py +12 -0
- bergson-0.0.1/bergson/attributor.py +159 -0
- bergson-0.0.1/bergson/build.py +261 -0
- bergson-0.0.1/bergson/collection.py +214 -0
- bergson-0.0.1/bergson/data.py +473 -0
- bergson-0.0.1/bergson/faiss_index.py +265 -0
- bergson-0.0.1/bergson/gradcheck.py +114 -0
- bergson-0.0.1/bergson/gradients.py +549 -0
- bergson-0.0.1/bergson/huggingface.py +384 -0
- bergson-0.0.1/bergson/math.py +102 -0
- bergson-0.0.1/bergson/peft.py +38 -0
- bergson-0.0.1/bergson/plot_eta.py +29 -0
- bergson-0.0.1/bergson/tmp.py +11 -0
- bergson-0.0.1/bergson/utils.py +60 -0
- bergson-0.0.1/bergson.egg-info/PKG-INFO +158 -0
- bergson-0.0.1/bergson.egg-info/SOURCES.txt +28 -0
- bergson-0.0.1/bergson.egg-info/dependency_links.txt +1 -0
- bergson-0.0.1/bergson.egg-info/entry_points.txt +2 -0
- bergson-0.0.1/bergson.egg-info/requires.txt +18 -0
- bergson-0.0.1/bergson.egg-info/top_level.txt +1 -0
- bergson-0.0.1/pyproject.toml +74 -0
- bergson-0.0.1/setup.cfg +4 -0
- bergson-0.0.1/tests/test_callback.py +251 -0
- bergson-0.0.1/tests/test_gradcheck.py +86 -0
- bergson-0.0.1/tests/test_gradients.py +104 -0
- bergson-0.0.1/tests/test_shrinkage.py +68 -0
bergson-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 EleutherAI
|
|
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.
|
bergson-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bergson
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Tracing the memory of neural nets with data attribution
|
|
5
|
+
License: MIT License
|
|
6
|
+
Keywords: interpretability,explainable-ai
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: accelerate
|
|
11
|
+
Requires-Dist: datasets
|
|
12
|
+
Requires-Dist: natsort
|
|
13
|
+
Requires-Dist: peft>=0.17.0
|
|
14
|
+
Requires-Dist: simple-parsing
|
|
15
|
+
Requires-Dist: torch
|
|
16
|
+
Requires-Dist: transformers
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
19
|
+
Requires-Dist: trl; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest; extra == "dev"
|
|
21
|
+
Provides-Extra: example
|
|
22
|
+
Requires-Dist: trl; extra == "example"
|
|
23
|
+
Provides-Extra: faiss
|
|
24
|
+
Requires-Dist: faiss-gpu-cu12; extra == "faiss"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Bergson
|
|
28
|
+
This library enables you to trace the memory of deep neural nets with gradient-based data attribution techniques. We currently focus on TrackStar, as described in [Scalable Influence and Fact Tracing for Large Language Model Pretraining](https://arxiv.org/abs/2410.17413v3) by Chang et al. (2024), although we plan to add support for other methods inspired by influence functions in the near future.
|
|
29
|
+
|
|
30
|
+
We view attribution as a counterfactual question: **_If we "unlearned" this training sample, how would the model's behavior change?_** This formulation ties attribution to some notion of what it means to "unlearn" a training sample. Here we focus on a very simple notion of unlearning: taking a gradient _ascent_ step on the loss with respect to the training sample. To mimic the behavior of popular optimizers, we precondition the gradient using Adam or Adafactor-style estimates of the second moments of the gradient.
|
|
31
|
+
|
|
32
|
+
# Announcements
|
|
33
|
+
|
|
34
|
+
**September 2025**
|
|
35
|
+
- Saving per-head gradients: https://github.com/EleutherAI/bergson/pull/40
|
|
36
|
+
- Eigendecompositions of preconditioners: https://github.com/EleutherAI/bergson/pull/34
|
|
37
|
+
- Dr. GRPO-based loss gradients: https://github.com/EleutherAI/bergson/pull/35
|
|
38
|
+
- Choosing between summing and averaging losses across tokens: https://github.com/EleutherAI/bergson/pull/36
|
|
39
|
+
- Saving the order training data is seen in while using the gradient collector callback for HF's Trainer/SFTTrainer: https://github.com/EleutherAI/bergson/pull/40
|
|
40
|
+
- Saving training gradients adds a ~17% wall clock overhead
|
|
41
|
+
- Improved static index build ETA accuracy: https://github.com/EleutherAI/bergson/pull/41
|
|
42
|
+
- Several small quality of life improvements for querying indexes: https://github.com/EleutherAI/bergson/pull/38
|
|
43
|
+
|
|
44
|
+
# Installation
|
|
45
|
+
|
|
46
|
+
We're not yet on PyPI, but you can `git clone` the repo and install it as a package using pip:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/EleutherAI/bergson.git
|
|
50
|
+
cd bergson
|
|
51
|
+
pip install .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
# Usage
|
|
55
|
+
The first step is to build an index of gradients for each training sample. You can do this from the command line, using `bergson` as a CLI tool:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bergson <output_path> --model <model_name> --dataset <dataset_name>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This will create a directory at `<output_path>` containing the gradients for each training sample in the specified dataset. The `--model` and `--dataset` arguments should be compatible with the Hugging Face `transformers` library. By default it assumes that the dataset has a `text` column, but you can specify other columns using `--prompt_column` and optionally `--completion_column`. The `--help` flag will show you all available options.
|
|
62
|
+
|
|
63
|
+
You can also use the library programmatically to build the index. The `collect_gradients` function is just a bit lower level the CLI tool, and allows you to specify the model and dataset directly as arguments. The result is a HuggingFace dataset which contains a handful of new columns, including `gradients`, which contains the gradients for each training sample. You can then use this dataset to compute attributions.
|
|
64
|
+
|
|
65
|
+
At the lowest level of abstraction, the `GradientCollector` context manager allows you to efficiently collect gradients for _each individual example_ in a batch during a backward pass, simultaneously randomly projecting the gradients to a lower-dimensional space to save memory. If you use Adafactor normalization, which is the default, we will do this in a very compute-efficient way which avoids computing the full gradient for each example before projecting it to the lower dimension. There are two main ways you can use `GradientCollector`:
|
|
66
|
+
|
|
67
|
+
1. Using a `closure` argument, which enables you to make use of the per-example gradients immediately after they are computed, during the backward pass. If you're computing summary statistics or other per-example metrics, this is the most efficient way to do it.
|
|
68
|
+
2. Without a `closure` argument, in which case the gradients are collected and returned as a dictionary mapping module names to batches of gradients. This is the simplest and most flexible approach but is a bit more memory-intensive.
|
|
69
|
+
|
|
70
|
+
## Training Gradients
|
|
71
|
+
|
|
72
|
+
Gradient collection during training is supported via an integration with HuggingFace's Trainer and SFTTrainer classes. Training gradients are saved in the original order corresponding to their dataset items, and when the `track_order` flag is set the training steps associated with each training item are separately saved.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from bergson import GradientCollectorCallback, prepare_for_gradient_collection
|
|
76
|
+
|
|
77
|
+
callback = GradientCollectorCallback(
|
|
78
|
+
path="runs/example",
|
|
79
|
+
track_order=True,
|
|
80
|
+
use_optimizer_state=False,
|
|
81
|
+
)
|
|
82
|
+
trainer = Trainer(
|
|
83
|
+
model=model,
|
|
84
|
+
args=training_args,
|
|
85
|
+
train_dataset=dataset,
|
|
86
|
+
eval_dataset=dataset,
|
|
87
|
+
callbacks=[callback],
|
|
88
|
+
)
|
|
89
|
+
trainer = prepare_for_gradient_collection(trainer)
|
|
90
|
+
trainer.train()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Attention Head Gradients
|
|
94
|
+
|
|
95
|
+
By default Bergson collects gradients for named parameter matrices, but gradients for individual attention heads within a named matrix can be collected too. To collect head gradients add a `head_cfgs` dictionary to the training calllback or static index config.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from bergson import HeadConfig, IndexConfig, DataConfig
|
|
99
|
+
from transformers import AutoModelForCausalLM
|
|
100
|
+
|
|
101
|
+
model = AutoModelForCausalLM.from_pretrained("RonenEldan/TinyStories-1M", trust_remote_code=True, use_safetensors=True)
|
|
102
|
+
|
|
103
|
+
collect_gradients(
|
|
104
|
+
model=model,
|
|
105
|
+
data=data,
|
|
106
|
+
processor=processor,
|
|
107
|
+
path="runs/example_with_heads",
|
|
108
|
+
head_cfgs={
|
|
109
|
+
# Head configuration for the TinyStories-1M transformer
|
|
110
|
+
"h.0.attn.attention.out_proj": HeadConfig(num_heads=16, head_size=4, head_dim=2),
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## GRPO
|
|
116
|
+
|
|
117
|
+
Where a reward signal is available we compute gradients using a weighted advantage estimate based on Dr. GRPO:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
bergson <output_path> --model <model_name> --dataset <dataset_name> --reward_column <reward_column_name>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Queries
|
|
124
|
+
|
|
125
|
+
We provide a query Attributor which supports unit normalized gradients and KNN search out of the box.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
from bergson import Attributor, FaissConfig
|
|
129
|
+
|
|
130
|
+
attr = Attributor(args.index, device="cuda")
|
|
131
|
+
|
|
132
|
+
...
|
|
133
|
+
query_tokens = tokenizer(query, return_tensors="pt").to("cuda:0")["input_ids"]
|
|
134
|
+
|
|
135
|
+
# Query the index
|
|
136
|
+
with attr.trace(model.base_model, 5) as result:
|
|
137
|
+
model(query_tokens, labels=query_tokens).loss.backward()
|
|
138
|
+
model.zero_grad()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
To efficiently query on-disk indexes, perform ANN searches, and explore many other scalability features add a FAISS config:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
attr = Attributor(args.index, device="cuda", faiss_cfg=FaissConfig("IVF1,SQfp16", mmap_index=True))
|
|
145
|
+
|
|
146
|
+
with attr.trace(model.base_model, 5) as result:
|
|
147
|
+
model(query_tokens, labels=query_tokens).loss.backward()
|
|
148
|
+
model.zero_grad()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
# Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
pip install -e .[dev]
|
|
155
|
+
pytest
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) for releases.
|
bergson-0.0.1/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Bergson
|
|
2
|
+
This library enables you to trace the memory of deep neural nets with gradient-based data attribution techniques. We currently focus on TrackStar, as described in [Scalable Influence and Fact Tracing for Large Language Model Pretraining](https://arxiv.org/abs/2410.17413v3) by Chang et al. (2024), although we plan to add support for other methods inspired by influence functions in the near future.
|
|
3
|
+
|
|
4
|
+
We view attribution as a counterfactual question: **_If we "unlearned" this training sample, how would the model's behavior change?_** This formulation ties attribution to some notion of what it means to "unlearn" a training sample. Here we focus on a very simple notion of unlearning: taking a gradient _ascent_ step on the loss with respect to the training sample. To mimic the behavior of popular optimizers, we precondition the gradient using Adam or Adafactor-style estimates of the second moments of the gradient.
|
|
5
|
+
|
|
6
|
+
# Announcements
|
|
7
|
+
|
|
8
|
+
**September 2025**
|
|
9
|
+
- Saving per-head gradients: https://github.com/EleutherAI/bergson/pull/40
|
|
10
|
+
- Eigendecompositions of preconditioners: https://github.com/EleutherAI/bergson/pull/34
|
|
11
|
+
- Dr. GRPO-based loss gradients: https://github.com/EleutherAI/bergson/pull/35
|
|
12
|
+
- Choosing between summing and averaging losses across tokens: https://github.com/EleutherAI/bergson/pull/36
|
|
13
|
+
- Saving the order training data is seen in while using the gradient collector callback for HF's Trainer/SFTTrainer: https://github.com/EleutherAI/bergson/pull/40
|
|
14
|
+
- Saving training gradients adds a ~17% wall clock overhead
|
|
15
|
+
- Improved static index build ETA accuracy: https://github.com/EleutherAI/bergson/pull/41
|
|
16
|
+
- Several small quality of life improvements for querying indexes: https://github.com/EleutherAI/bergson/pull/38
|
|
17
|
+
|
|
18
|
+
# Installation
|
|
19
|
+
|
|
20
|
+
We're not yet on PyPI, but you can `git clone` the repo and install it as a package using pip:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/EleutherAI/bergson.git
|
|
24
|
+
cd bergson
|
|
25
|
+
pip install .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
# Usage
|
|
29
|
+
The first step is to build an index of gradients for each training sample. You can do this from the command line, using `bergson` as a CLI tool:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
bergson <output_path> --model <model_name> --dataset <dataset_name>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This will create a directory at `<output_path>` containing the gradients for each training sample in the specified dataset. The `--model` and `--dataset` arguments should be compatible with the Hugging Face `transformers` library. By default it assumes that the dataset has a `text` column, but you can specify other columns using `--prompt_column` and optionally `--completion_column`. The `--help` flag will show you all available options.
|
|
36
|
+
|
|
37
|
+
You can also use the library programmatically to build the index. The `collect_gradients` function is just a bit lower level the CLI tool, and allows you to specify the model and dataset directly as arguments. The result is a HuggingFace dataset which contains a handful of new columns, including `gradients`, which contains the gradients for each training sample. You can then use this dataset to compute attributions.
|
|
38
|
+
|
|
39
|
+
At the lowest level of abstraction, the `GradientCollector` context manager allows you to efficiently collect gradients for _each individual example_ in a batch during a backward pass, simultaneously randomly projecting the gradients to a lower-dimensional space to save memory. If you use Adafactor normalization, which is the default, we will do this in a very compute-efficient way which avoids computing the full gradient for each example before projecting it to the lower dimension. There are two main ways you can use `GradientCollector`:
|
|
40
|
+
|
|
41
|
+
1. Using a `closure` argument, which enables you to make use of the per-example gradients immediately after they are computed, during the backward pass. If you're computing summary statistics or other per-example metrics, this is the most efficient way to do it.
|
|
42
|
+
2. Without a `closure` argument, in which case the gradients are collected and returned as a dictionary mapping module names to batches of gradients. This is the simplest and most flexible approach but is a bit more memory-intensive.
|
|
43
|
+
|
|
44
|
+
## Training Gradients
|
|
45
|
+
|
|
46
|
+
Gradient collection during training is supported via an integration with HuggingFace's Trainer and SFTTrainer classes. Training gradients are saved in the original order corresponding to their dataset items, and when the `track_order` flag is set the training steps associated with each training item are separately saved.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from bergson import GradientCollectorCallback, prepare_for_gradient_collection
|
|
50
|
+
|
|
51
|
+
callback = GradientCollectorCallback(
|
|
52
|
+
path="runs/example",
|
|
53
|
+
track_order=True,
|
|
54
|
+
use_optimizer_state=False,
|
|
55
|
+
)
|
|
56
|
+
trainer = Trainer(
|
|
57
|
+
model=model,
|
|
58
|
+
args=training_args,
|
|
59
|
+
train_dataset=dataset,
|
|
60
|
+
eval_dataset=dataset,
|
|
61
|
+
callbacks=[callback],
|
|
62
|
+
)
|
|
63
|
+
trainer = prepare_for_gradient_collection(trainer)
|
|
64
|
+
trainer.train()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Attention Head Gradients
|
|
68
|
+
|
|
69
|
+
By default Bergson collects gradients for named parameter matrices, but gradients for individual attention heads within a named matrix can be collected too. To collect head gradients add a `head_cfgs` dictionary to the training calllback or static index config.
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from bergson import HeadConfig, IndexConfig, DataConfig
|
|
73
|
+
from transformers import AutoModelForCausalLM
|
|
74
|
+
|
|
75
|
+
model = AutoModelForCausalLM.from_pretrained("RonenEldan/TinyStories-1M", trust_remote_code=True, use_safetensors=True)
|
|
76
|
+
|
|
77
|
+
collect_gradients(
|
|
78
|
+
model=model,
|
|
79
|
+
data=data,
|
|
80
|
+
processor=processor,
|
|
81
|
+
path="runs/example_with_heads",
|
|
82
|
+
head_cfgs={
|
|
83
|
+
# Head configuration for the TinyStories-1M transformer
|
|
84
|
+
"h.0.attn.attention.out_proj": HeadConfig(num_heads=16, head_size=4, head_dim=2),
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## GRPO
|
|
90
|
+
|
|
91
|
+
Where a reward signal is available we compute gradients using a weighted advantage estimate based on Dr. GRPO:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
bergson <output_path> --model <model_name> --dataset <dataset_name> --reward_column <reward_column_name>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Queries
|
|
98
|
+
|
|
99
|
+
We provide a query Attributor which supports unit normalized gradients and KNN search out of the box.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
from bergson import Attributor, FaissConfig
|
|
103
|
+
|
|
104
|
+
attr = Attributor(args.index, device="cuda")
|
|
105
|
+
|
|
106
|
+
...
|
|
107
|
+
query_tokens = tokenizer(query, return_tensors="pt").to("cuda:0")["input_ids"]
|
|
108
|
+
|
|
109
|
+
# Query the index
|
|
110
|
+
with attr.trace(model.base_model, 5) as result:
|
|
111
|
+
model(query_tokens, labels=query_tokens).loss.backward()
|
|
112
|
+
model.zero_grad()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
To efficiently query on-disk indexes, perform ANN searches, and explore many other scalability features add a FAISS config:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
attr = Attributor(args.index, device="cuda", faiss_cfg=FaissConfig("IVF1,SQfp16", mmap_index=True))
|
|
119
|
+
|
|
120
|
+
with attr.trace(model.base_model, 5) as result:
|
|
121
|
+
model(query_tokens, labels=query_tokens).loss.backward()
|
|
122
|
+
model.zero_grad()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
# Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install -e .[dev]
|
|
129
|
+
pytest
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) for releases.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
2
|
+
|
|
3
|
+
from .attributor import Attributor
|
|
4
|
+
from .collection import collect_gradients
|
|
5
|
+
from .data import DataConfig, IndexConfig, load_gradients
|
|
6
|
+
from .faiss_index import FaissConfig
|
|
7
|
+
from .gradcheck import FiniteDiff
|
|
8
|
+
from .gradients import GradientCollector, GradientProcessor, HeadConfig
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"collect_gradients",
|
|
12
|
+
"load_gradients",
|
|
13
|
+
"Attributor",
|
|
14
|
+
"FaissConfig",
|
|
15
|
+
"FiniteDiff",
|
|
16
|
+
"GradientCollector",
|
|
17
|
+
"GradientProcessor",
|
|
18
|
+
"IndexConfig",
|
|
19
|
+
"DataConfig",
|
|
20
|
+
"HeadConfig",
|
|
21
|
+
]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from typing import Generator
|
|
4
|
+
|
|
5
|
+
import torch
|
|
6
|
+
from torch import Tensor, nn
|
|
7
|
+
|
|
8
|
+
from .data import load_gradients
|
|
9
|
+
from .faiss_index import FaissConfig, FaissIndex
|
|
10
|
+
from .gradients import GradientCollector, GradientProcessor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TraceResult:
|
|
14
|
+
"""Result of a .trace() call."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
# Should be set by the Attributor after a search
|
|
18
|
+
self._indices: Tensor | None = None
|
|
19
|
+
self._scores: Tensor | None = None
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def indices(self) -> Tensor:
|
|
23
|
+
"""The indices of the top-k examples."""
|
|
24
|
+
if self._indices is None:
|
|
25
|
+
raise ValueError("No indices available. Exit the context manager first.")
|
|
26
|
+
|
|
27
|
+
return self._indices
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def scores(self) -> Tensor:
|
|
31
|
+
"""The attribution scores of the top-k examples."""
|
|
32
|
+
if self._scores is None:
|
|
33
|
+
raise ValueError("No scores available. Exit the context manager first.")
|
|
34
|
+
|
|
35
|
+
return self._scores
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Attributor:
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
index_path: str,
|
|
42
|
+
device: str = "cpu",
|
|
43
|
+
dtype: torch.dtype = torch.float32,
|
|
44
|
+
unit_norm: bool = False,
|
|
45
|
+
faiss_cfg: FaissConfig | None = None,
|
|
46
|
+
):
|
|
47
|
+
self.device = device
|
|
48
|
+
self.dtype = dtype
|
|
49
|
+
self.unit_norm = unit_norm
|
|
50
|
+
self.faiss_index = None
|
|
51
|
+
|
|
52
|
+
# Load the gradient processor
|
|
53
|
+
self.processor = GradientProcessor.load(index_path, map_location=device)
|
|
54
|
+
|
|
55
|
+
# Load the gradient index
|
|
56
|
+
if faiss_cfg:
|
|
57
|
+
self.faiss_index = FaissIndex(index_path, faiss_cfg, device, unit_norm)
|
|
58
|
+
self.N = self.faiss_index.ntotal
|
|
59
|
+
else:
|
|
60
|
+
mmap = load_gradients(index_path)
|
|
61
|
+
|
|
62
|
+
# Copy gradients into device memory
|
|
63
|
+
self.grads = {
|
|
64
|
+
name: torch.tensor(mmap[name], device=device, dtype=dtype)
|
|
65
|
+
for name in mmap.dtype.names
|
|
66
|
+
}
|
|
67
|
+
self.N = mmap[mmap.dtype.names[0]].shape[0]
|
|
68
|
+
|
|
69
|
+
if unit_norm:
|
|
70
|
+
norm = torch.cat([grad for grad in self.grads.values()], dim=1).norm(
|
|
71
|
+
dim=1, keepdim=True
|
|
72
|
+
)
|
|
73
|
+
for name in self.grads:
|
|
74
|
+
self.grads[name] /= norm
|
|
75
|
+
|
|
76
|
+
def search(
|
|
77
|
+
self, queries: dict[str, Tensor], k: int, modules: list[str] | None = None
|
|
78
|
+
) -> tuple[Tensor, Tensor]:
|
|
79
|
+
"""
|
|
80
|
+
Search for the `k` nearest examples in the index based on the query or queries.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
queries: The query tensor of shape [..., d].
|
|
84
|
+
k: The number of nearest examples to return for each query.
|
|
85
|
+
module: The name of the module to search for. If `None`,
|
|
86
|
+
all modules will be searched.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A namedtuple containing the top `k` indices and inner products for each
|
|
90
|
+
query. Both have shape [..., k].
|
|
91
|
+
"""
|
|
92
|
+
q = {name: item.to(self.device, self.dtype) for name, item in queries.items()}
|
|
93
|
+
|
|
94
|
+
if self.unit_norm:
|
|
95
|
+
norm = torch.cat(list(q.values()), dim=1).norm(dim=1, keepdim=True)
|
|
96
|
+
|
|
97
|
+
for name in q:
|
|
98
|
+
q[name] /= norm + 1e-8
|
|
99
|
+
|
|
100
|
+
if self.faiss_index:
|
|
101
|
+
if modules:
|
|
102
|
+
raise NotImplementedError(
|
|
103
|
+
"FAISS index does not implement module-specific search."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
q = torch.cat([q[name] for name in q], dim=1).cpu().numpy()
|
|
107
|
+
|
|
108
|
+
distances, indices = self.faiss_index.search(q, k)
|
|
109
|
+
|
|
110
|
+
return torch.from_numpy(distances.squeeze()), torch.from_numpy(
|
|
111
|
+
indices.squeeze()
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
modules = modules or list(q.keys())
|
|
115
|
+
k = min(k, self.N)
|
|
116
|
+
|
|
117
|
+
scores = torch.stack(
|
|
118
|
+
[q[name] @ self.grads[name].mT for name in modules], dim=-1
|
|
119
|
+
).sum(-1)
|
|
120
|
+
|
|
121
|
+
return torch.topk(scores, k)
|
|
122
|
+
|
|
123
|
+
@contextmanager
|
|
124
|
+
def trace(
|
|
125
|
+
self, module: nn.Module, k: int, *, precondition: bool = False
|
|
126
|
+
) -> Generator[TraceResult, None, None]:
|
|
127
|
+
"""
|
|
128
|
+
Context manager to trace the gradients of a module and return the
|
|
129
|
+
corresponding Attributor instance.
|
|
130
|
+
"""
|
|
131
|
+
mod_grads = defaultdict(list)
|
|
132
|
+
result = TraceResult()
|
|
133
|
+
|
|
134
|
+
def callback(name: str, g: Tensor):
|
|
135
|
+
# Precondition the gradient using Cholesky solve
|
|
136
|
+
if precondition:
|
|
137
|
+
eigval, eigvec = self.processor.preconditioners_eigen[name]
|
|
138
|
+
eigval_inverse_sqrt = 1.0 / (eigval).sqrt()
|
|
139
|
+
P = eigvec * eigval_inverse_sqrt @ eigvec.mT
|
|
140
|
+
g = g.flatten(1).type_as(P)
|
|
141
|
+
g = g @ P
|
|
142
|
+
else:
|
|
143
|
+
g = g.flatten(1)
|
|
144
|
+
|
|
145
|
+
# Store the gradient for later use
|
|
146
|
+
mod_grads[name].append(g.to(self.device, self.dtype, non_blocking=True))
|
|
147
|
+
|
|
148
|
+
with GradientCollector(module, callback, self.processor):
|
|
149
|
+
yield result
|
|
150
|
+
|
|
151
|
+
if not mod_grads:
|
|
152
|
+
raise ValueError("No grads collected. Did you forget to call backward?")
|
|
153
|
+
|
|
154
|
+
queries = {name: torch.cat(g, dim=1) for name, g in mod_grads.items()}
|
|
155
|
+
|
|
156
|
+
if any(q.isnan().any() for q in queries.values()):
|
|
157
|
+
raise ValueError("NaN found in queries.")
|
|
158
|
+
|
|
159
|
+
result._scores, result._indices = self.search(queries, k)
|