grantner commited on
Commit
e4614a9
·
verified ·
1 Parent(s): 488984b

feat: chunkable transformer and last index pooling

Browse files
Files changed (3) hide show
  1. .gitattributes +35 -0
  2. README.md +76 -0
  3. models.py +185 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: sentence-transformers
4
+ tags:
5
+ - transformers
6
+ - sentence-transformers
7
+ - vertical-chunking
8
+ ---
9
+
10
+ # chunkable-sentence-transformer
11
+
12
+ Custom [Sentence Transformers](https://www.sbert.net/) modules that add support for **vertically chunked inference** to encode arbitrarily long texts with constant memory by processing fixed-size vertical chunks through all model layers sequentially.
13
+
14
+ ## What this repository provides
15
+
16
+ - **`ChunkableTransformer`:** extends the Sentence Transformers `Transformer` module with a `vertical_chunk_size` parameter. When set, input sequences are split into chunks that are each processed through the full model depth, carrying the recurrent states across chunks instead of materializing the entire sequence in memory at once.
17
+ - **`LastIndexPooling`:** pools the embedding from the last token regardless of padding, which allows us to only retain outputs of the final chunk when using left padding with chunked inference.
18
+
19
+ ## Usage
20
+
21
+ This repository is designed to be referenced directly from Hugging Face model configs via `modules.json`, so that models can be loaded with `trust_remote_code=True` without any local installation:
22
+
23
+ ```json
24
+ [
25
+ {
26
+ "idx": 0,
27
+ "name": "0",
28
+ "path": "",
29
+ "type": "dynatrace-oss/chunkable-sentence-transformer--models.ChunkableTransformer"
30
+ },
31
+ {
32
+ "idx": 1,
33
+ "name": "1",
34
+ "path": "1_LastIndexPooling",
35
+ "type": "dynatrace-oss/chunkable-sentence-transformer--models.LastIndexPooling"
36
+ }
37
+ ]
38
+ ```
39
+
40
+ Constant-memory inference is then available via the `vertical_chunk_size` encode parameter:
41
+
42
+ ```python
43
+ from sentence_transformers import SentenceTransformer
44
+
45
+ model = SentenceTransformer("dynatrace-oss/llama-embed-mamba2-7b", trust_remote_code=True)
46
+ embeddings = model.encode(["Your long document text here..."], vertical_chunk_size=512)
47
+ ```
48
+
49
+ ## Requirements
50
+
51
+ ```bash
52
+ pip install sentence-transformers
53
+ ```
54
+
55
+ ## Models
56
+
57
+ This code was created for the following embedding models:
58
+
59
+ - [dynatrace-oss/llama-embed-mamba2-7b](https://huggingface.co/dynatrace-oss/llama-embed-mamba2-7b)
60
+ - [dynatrace-oss/llama-embed-mamba2-1.3b](https://huggingface.co/dynatrace-oss/llama-embed-mamba2-1.3b)
61
+
62
+ ## Open Source Integration Roadmap
63
+
64
+ Our goal is to integrate all necessary changes to simplify the adoption of vertically chunked inference for other models:
65
+
66
+ > [!Note]
67
+ > ⚪ Planned | 🟡 In Progress | 🟢 Integrated
68
+
69
+ - ⚪ **sentence-transformers:** Last index pooling
70
+ - ⚪ **sentence-transformers:** Native vertical chunking support for transformers
71
+
72
+ *This list will be updated as integration progresses.*
73
+
74
+ ## License
75
+
76
+ Apache-2.0
models.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sentence_transformers.models import Transformer, Module
2
+ from transformers.utils.import_utils import is_peft_available
3
+ import torch
4
+
5
+
6
+ class ChunkableTransformer(Transformer):
7
+ config_keys = Transformer.config_keys + ["output_all_token_embeddings"]
8
+
9
+ def __init__(self, *args, output_all_token_embeddings=True, **kwargs):
10
+ super().__init__(*args, **kwargs)
11
+ self.cache_param_name = (
12
+ "past_key_values"
13
+ if "past_key_values" in self.model_forward_params
14
+ else "cache_params"
15
+ )
16
+ self.model_forward_params = self.model_forward_params - {
17
+ "use_cache",
18
+ "past_key_values",
19
+ "cache_params",
20
+ }
21
+ self.forward_kwargs = set(self.forward_kwargs) | {
22
+ "vertical_chunk_size",
23
+ "output_all_token_embeddings",
24
+ }
25
+ self.output_all_token_embeddings = output_all_token_embeddings
26
+
27
+ def __repr__(self) -> str:
28
+ return f"ChunkableTransformer({dict(self.get_config_dict(), architecture=self.auto_model.__class__.__name__)})"
29
+
30
+ def forward(
31
+ self,
32
+ features: dict[str, torch.Tensor],
33
+ vertical_chunk_size: int | None = None,
34
+ output_all_token_embeddings: bool | None = None,
35
+ **kwargs,
36
+ ) -> dict[str, torch.Tensor]:
37
+ """
38
+ Forward pass through the transformer model.
39
+
40
+ This method processes the input features through the underlying transformers model
41
+ and returns the token embeddings along with any other relevant outputs.
42
+
43
+ Notes:
44
+ - Only passes arguments that are expected by the underlying transformer model
45
+
46
+ Args:
47
+ features (dict[str, torch.Tensor]): Input features dictionary containing at least
48
+ 'input_ids' and 'attention_mask'. May also contain other tensors required by
49
+ the underlying transformer model.
50
+ vertical_chunk_size (int | None): Size of vertical chunks for processing. If None, no chunking is applied.
51
+ output_all_token_embeddings (bool | None): Whether to return all token embeddings or only the token embeddings of the last vertical chunk. If None, uses the default value from the model configuration.
52
+ **kwargs: Additional keyword arguments to pass to the underlying transformer model.
53
+
54
+ Returns:
55
+ dict[str, torch.Tensor]: Updated features dictionary containing the input features, plus:
56
+ - 'token_embeddings': Token-level embeddings from the transformer model
57
+ - 'attention_mask': Possibly modified attention mask if using PeftModel with prompt learning
58
+ - 'all_layer_embeddings': If the model outputs hidden states, contains embeddings from all layers
59
+ """
60
+ trans_features = {
61
+ key: value
62
+ for key, value in features.items()
63
+ if key in self.model_forward_params
64
+ }
65
+
66
+ input_length = features.get("input_ids", features.get("inputs_embeds")).size(1)
67
+ output_all_token_embeddings = (
68
+ output_all_token_embeddings
69
+ if output_all_token_embeddings is not None
70
+ else self.output_all_token_embeddings
71
+ )
72
+ output_hidden_states = trans_features.get(
73
+ "output_hidden_states", self.auto_model.config.output_hidden_states
74
+ )
75
+ use_vertical_chunking = (
76
+ vertical_chunk_size is not None and input_length > vertical_chunk_size
77
+ )
78
+ all_token_embedding_chunks = None
79
+ all_hidden_states = None
80
+
81
+ if use_vertical_chunking:
82
+ cache_params = None
83
+ chunked_features = {
84
+ k: v.split(vertical_chunk_size, dim=1)
85
+ for k, v in trans_features.items()
86
+ if k != "attention_mask"
87
+ }
88
+
89
+ if "attention_mask" in trans_features:
90
+ chunked_features["attention_mask"] = (
91
+ trans_features["attention_mask"][:, :chunk_index]
92
+ for chunk_index in range(
93
+ vertical_chunk_size,
94
+ input_length + vertical_chunk_size,
95
+ vertical_chunk_size,
96
+ )
97
+ )
98
+
99
+ # pivot from dict of lists to list of dicts
100
+ chunked_features = map(
101
+ dict,
102
+ [
103
+ zip(chunked_features, values, strict=True)
104
+ for values in zip(*chunked_features.values(), strict=True)
105
+ ],
106
+ )
107
+
108
+ if output_all_token_embeddings:
109
+ all_token_embedding_chunks = ()
110
+
111
+ if output_hidden_states:
112
+ all_hidden_states = ()
113
+
114
+ for chunk_features in chunked_features:
115
+ outputs = self.auto_model(
116
+ **chunk_features,
117
+ **kwargs,
118
+ use_cache=True,
119
+ cache_params=cache_params,
120
+ return_dict=True,
121
+ )
122
+ cache_params = getattr(outputs, self.cache_param_name)
123
+
124
+ if all_token_embedding_chunks is not None:
125
+ all_token_embedding_chunks += (outputs[0],)
126
+ if all_hidden_states is not None and "hidden_states" in outputs:
127
+ all_hidden_states += (outputs["hidden_states"],)
128
+
129
+ if output_all_token_embeddings:
130
+ token_embeddings = torch.cat(all_token_embedding_chunks, dim=1)
131
+ else:
132
+ token_embeddings = outputs[0]
133
+ else:
134
+ outputs = self.auto_model(**trans_features, **kwargs, return_dict=True)
135
+
136
+ if all_token_embedding_chunks is not None:
137
+ features["token_embeddings"] = torch.cat(all_token_embedding_chunks, dim=1)
138
+ else:
139
+ features["token_embeddings"] = outputs[0]
140
+
141
+ if all_hidden_states is not None and len(all_hidden_states) > 0:
142
+ features["all_layer_embeddings"] = torch.cat(all_hidden_states, dim=1)
143
+ elif self.auto_model.config.output_hidden_states and "hidden_states" in outputs:
144
+ features["all_layer_embeddings"] = outputs.get("hidden_states", None)
145
+
146
+ # If the AutoModel is wrapped with a PeftModelForFeatureExtraction, then it may have added virtual tokens
147
+ # We need to extend the attention mask to include these virtual tokens, or the pooling will fail
148
+ if is_peft_available():
149
+ from peft import PeftModelForFeatureExtraction
150
+
151
+ if (
152
+ isinstance(self.auto_model, PeftModelForFeatureExtraction)
153
+ and self.auto_model.active_peft_config.is_prompt_learning
154
+ ):
155
+ batch_size = token_embeddings.size(0)
156
+ attention_mask = features["attention_mask"]
157
+ prefix_attention_mask = torch.ones(
158
+ batch_size,
159
+ self.auto_model.active_peft_config.num_virtual_tokens,
160
+ device=attention_mask.device,
161
+ )
162
+ features["attention_mask"] = torch.cat((prefix_attention_mask, attention_mask), dim=1)
163
+
164
+ return features
165
+
166
+
167
+ class LastIndexPooling(Module):
168
+ """Pools the embedding of the last token in the sequence.
169
+
170
+ Note: This module ignores ``attention_mask`` and always returns the embedding
171
+ at index ``-1``. It therefore requires inputs to be left-padded so that
172
+ the final non-pad token aligns with the last position. Combined with
173
+ ``ChunkableTransformer``, this makes it safe to discard all but the final
174
+ vertical chunk's outputs during chunked inference.
175
+ """
176
+
177
+ def __repr__(self) -> str:
178
+ return f"LastIndexPooling({self.get_config_dict()})"
179
+
180
+ def save(self, *args, **kwargs) -> None:
181
+ return
182
+
183
+ def forward(self, features: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:
184
+ features["sentence_embedding"] = features["token_embeddings"][:, -1]
185
+ return features