Instructions to use dynatrace-oss/chunkable-sentence-transformer with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use dynatrace-oss/chunkable-sentence-transformer with sentence-transformers:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("dynatrace-oss/chunkable-sentence-transformer") sentences = [ "The weather is lovely today.", "It's so sunny outside!", "He drove to the stadium." ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [3, 3] - Transformers
How to use dynatrace-oss/chunkable-sentence-transformer with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("dynatrace-oss/chunkable-sentence-transformer", device_map="auto") - Notebooks
- Google Colab
- Kaggle
feat: chunkable transformer and last index pooling
Browse files- .gitattributes +35 -0
- README.md +76 -0
- 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
|