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 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.
@@ -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,12 @@
1
+ from simple_parsing import parse
2
+
3
+ from .build import build_gradient_dataset
4
+ from .data import IndexConfig
5
+
6
+
7
+ def main():
8
+ build_gradient_dataset(parse(IndexConfig))
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -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)