anthracite 1.0.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.
- anthracite/__init__.py +222 -0
- anthracite/architectures/__init__.py +9 -0
- anthracite/architectures/anthracite1_embedding.py +231 -0
- anthracite/architectures/anthracite1_text.py +309 -0
- anthracite/cli.py +161 -0
- anthracite/core/__init__.py +4 -0
- anthracite/core/config.py +209 -0
- anthracite/core/exceptions.py +62 -0
- anthracite/core/finetuner.py +292 -0
- anthracite/core/registry.py +98 -0
- anthracite/core/trainer.py +487 -0
- anthracite/datasets/__init__.py +9 -0
- anthracite/datasets/hf.py +58 -0
- anthracite/datasets/loader.py +139 -0
- anthracite/datasets/local.py +235 -0
- anthracite/datasets/pairs.py +125 -0
- anthracite/datasets/text.py +142 -0
- anthracite/devices/__init__.py +3 -0
- anthracite/devices/auto.py +147 -0
- anthracite/devices/cpu.py +54 -0
- anthracite/devices/cuda.py +69 -0
- anthracite/devices/tpu.py +68 -0
- anthracite/inference/__init__.py +13 -0
- anthracite/inference/embedding.py +90 -0
- anthracite/inference/loader.py +86 -0
- anthracite/inference/text.py +64 -0
- anthracite/interface/__init__.py +3 -0
- anthracite/interface/generator.py +215 -0
- anthracite/io/__init__.py +4 -0
- anthracite/io/config.py +65 -0
- anthracite/io/metadata.py +157 -0
- anthracite/io/safetensors.py +147 -0
- anthracite/tokenizer/__init__.py +5 -0
- anthracite/tokenizer/builder.py +218 -0
- anthracite/tokenizer/tokenizer.py +230 -0
- anthracite/tokenizer/vocabulary.py +91 -0
- anthracite/training/__init__.py +18 -0
- anthracite/training/checkpoint.py +145 -0
- anthracite/training/loop.py +227 -0
- anthracite/training/memory.py +165 -0
- anthracite/training/optimizer.py +38 -0
- anthracite/training/precision.py +94 -0
- anthracite/training/scheduler.py +64 -0
- anthracite/utils/logging.py +105 -0
- anthracite/utils/parameters.py +187 -0
- anthracite/utils/progress.py +66 -0
- anthracite/utils/seed.py +76 -0
- anthracite-1.0.0.dist-info/METADATA +500 -0
- anthracite-1.0.0.dist-info/RECORD +53 -0
- anthracite-1.0.0.dist-info/WHEEL +5 -0
- anthracite-1.0.0.dist-info/entry_points.txt +2 -0
- anthracite-1.0.0.dist-info/licenses/LICENSE +15 -0
- anthracite-1.0.0.dist-info/top_level.txt +1 -0
anthracite/__init__.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Anthracite – universal AI training & fine-tuning framework.
|
|
2
|
+
|
|
3
|
+
Train a model in one call::
|
|
4
|
+
|
|
5
|
+
from anthracite import train
|
|
6
|
+
|
|
7
|
+
train(
|
|
8
|
+
model_name="Nutral-GPT-20M",
|
|
9
|
+
model_type="text_gen",
|
|
10
|
+
dataset="my_dataset.jsonl",
|
|
11
|
+
tokens=100_000_000,
|
|
12
|
+
params=20_000_000,
|
|
13
|
+
context_length=512,
|
|
14
|
+
device="auto",
|
|
15
|
+
batch_size="auto",
|
|
16
|
+
precision="auto",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Then use it::
|
|
20
|
+
|
|
21
|
+
from anthracite import generate
|
|
22
|
+
print(generate(model="./models/Nutral-GPT-20M", prompt="Hello, my name is"))
|
|
23
|
+
|
|
24
|
+
Embedding models work the same way::
|
|
25
|
+
|
|
26
|
+
from anthracite import train, embed, similarity
|
|
27
|
+
|
|
28
|
+
train(model_name="MyEmbedder", model_type="embedding", dataset="corpus.txt",
|
|
29
|
+
tokens=20_000_000, params="30M", context_length=256)
|
|
30
|
+
|
|
31
|
+
vectors = embed("./models/MyEmbedder", ["a first sentence", "a second one"])
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from .core.config import ANTHRACITE_VERSION as __version__
|
|
37
|
+
from .core.config import ModelConfig, TrainingConfig
|
|
38
|
+
from .core.exceptions import (
|
|
39
|
+
AnthraciteError,
|
|
40
|
+
ArchitectureError,
|
|
41
|
+
CheckpointError,
|
|
42
|
+
DatasetNotFoundError,
|
|
43
|
+
DeviceError,
|
|
44
|
+
GenerationError,
|
|
45
|
+
InsufficientMemoryError,
|
|
46
|
+
InvalidConfigurationError,
|
|
47
|
+
TPUNotAvailableError,
|
|
48
|
+
TokenizerError,
|
|
49
|
+
UnsupportedDatasetError,
|
|
50
|
+
UnsupportedModelTypeError,
|
|
51
|
+
)
|
|
52
|
+
from .tokenizer.tokenizer import AnthraciteTokenizer
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def train(
|
|
56
|
+
model_name: str = "anthracite-model",
|
|
57
|
+
model_type: str = "text_gen",
|
|
58
|
+
dataset=None,
|
|
59
|
+
tokens=10_000_000,
|
|
60
|
+
params="20M",
|
|
61
|
+
context_length: int = 512,
|
|
62
|
+
vocab_size: int = 32768,
|
|
63
|
+
batch_size="auto",
|
|
64
|
+
device: str = "auto",
|
|
65
|
+
precision: str = "auto",
|
|
66
|
+
**kwargs,
|
|
67
|
+
) -> dict:
|
|
68
|
+
"""Train a new model from scratch and save it as a SafeTensors package.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
model_name : name of the model (used for the output directory and config).
|
|
73
|
+
model_type : ``"text_gen"`` or ``"embedding"``.
|
|
74
|
+
dataset : path, directory, Hugging Face id, HF Dataset object or list of strings.
|
|
75
|
+
tokens : training token budget, e.g. ``100_000_000`` or ``"100M"``.
|
|
76
|
+
params : target parameter count, e.g. ``20_000_000`` or ``"20M"``.
|
|
77
|
+
context_length : sequence length for text models.
|
|
78
|
+
batch_size : ``"auto"`` or an integer micro-batch size.
|
|
79
|
+
device : ``"auto" | "cpu" | "cuda" | "cuda:N" | "tpu"``.
|
|
80
|
+
precision : ``"auto" | "fp32" | "fp16" | "bf16"``.
|
|
81
|
+
|
|
82
|
+
Extra keyword arguments are forwarded to :class:`~anthracite.core.config.TrainingConfig`
|
|
83
|
+
(``learning_rate``, ``checkpoint_interval``, ``resume``, ``seed``, ``output_dir``,
|
|
84
|
+
``objective``, ``pooling``, ``embedding_dim``, ``val_split`` …), and unknown keys
|
|
85
|
+
become architecture overrides
|
|
86
|
+
(``d_model``, ``n_layers``, ``n_heads`` …).
|
|
87
|
+
|
|
88
|
+
Returns the training metadata dictionary.
|
|
89
|
+
"""
|
|
90
|
+
from .core.trainer import train as _train
|
|
91
|
+
|
|
92
|
+
return _train(
|
|
93
|
+
model_name=model_name,
|
|
94
|
+
model_type=model_type,
|
|
95
|
+
dataset=dataset,
|
|
96
|
+
tokens=tokens,
|
|
97
|
+
params=params,
|
|
98
|
+
context_length=context_length,
|
|
99
|
+
vocab_size=vocab_size,
|
|
100
|
+
batch_size=batch_size,
|
|
101
|
+
device=device,
|
|
102
|
+
precision=precision,
|
|
103
|
+
**kwargs,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def finetune(
|
|
108
|
+
model=None,
|
|
109
|
+
dataset=None,
|
|
110
|
+
tokens=1_000_000,
|
|
111
|
+
device: str = "auto",
|
|
112
|
+
batch_size="auto",
|
|
113
|
+
precision: str = "auto",
|
|
114
|
+
**kwargs,
|
|
115
|
+
) -> dict:
|
|
116
|
+
"""Continue training an existing Anthracite model on new data.
|
|
117
|
+
|
|
118
|
+
``model`` may be a local model directory, an Anthracite checkpoint, or a
|
|
119
|
+
Hugging Face repo id. The base model is never overwritten: the result is
|
|
120
|
+
written to ``<name>-Finetuned`` unless ``output_dir`` says otherwise.
|
|
121
|
+
"""
|
|
122
|
+
from .core.finetuner import finetune as _finetune
|
|
123
|
+
|
|
124
|
+
return _finetune(
|
|
125
|
+
model=model,
|
|
126
|
+
dataset=dataset,
|
|
127
|
+
tokens=tokens,
|
|
128
|
+
device=device,
|
|
129
|
+
batch_size=batch_size,
|
|
130
|
+
precision=precision,
|
|
131
|
+
**kwargs,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load_model(model, device: str = "auto", precision: str | None = None, verbose: bool = False):
|
|
136
|
+
"""Load a trained model (config + architecture + weights + tokenizer) for inference."""
|
|
137
|
+
from .inference.loader import load_model as _load
|
|
138
|
+
|
|
139
|
+
return _load(model, device=device, precision=precision, verbose=verbose)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def generate(model, prompt: str = "", **kwargs):
|
|
143
|
+
"""Generate text from a trained ``text_gen`` model."""
|
|
144
|
+
from .inference.loader import LoadedModel
|
|
145
|
+
from .inference.loader import load_model as _load
|
|
146
|
+
from .inference.text import generate_text
|
|
147
|
+
|
|
148
|
+
bundle = model if isinstance(model, LoadedModel) else _load(model, device=kwargs.pop("device", "auto"))
|
|
149
|
+
if bundle.config.model_type == "embedding":
|
|
150
|
+
raise GenerationError(
|
|
151
|
+
f"{bundle.config.model_name} is an embedding model, so it has nothing to generate.",
|
|
152
|
+
"Use embed(), similarity() or search() instead.",
|
|
153
|
+
)
|
|
154
|
+
return generate_text(bundle, prompt, **kwargs)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def embed(model, texts, batch_size: int = 16, normalize: bool = True, **kwargs):
|
|
158
|
+
"""Encode text into vectors with a trained ``embedding`` model.
|
|
159
|
+
|
|
160
|
+
``texts`` may be one string or a list; returns a numpy array of shape
|
|
161
|
+
``(n, dimension)`` (or ``(dimension,)`` for a single string).
|
|
162
|
+
"""
|
|
163
|
+
from .inference.embedding import embed as _embed
|
|
164
|
+
|
|
165
|
+
return _embed(model, texts, batch_size=batch_size, normalize=normalize, **kwargs)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def similarity(model, a, b, **kwargs):
|
|
169
|
+
"""Cosine similarity between two strings (or two lists of strings)."""
|
|
170
|
+
from .inference.embedding import similarity as _similarity
|
|
171
|
+
|
|
172
|
+
return _similarity(model, a, b, **kwargs)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def search(model, query: str, documents, top_k: int = 5, **kwargs) -> list:
|
|
176
|
+
"""Rank documents against a query; returns ``[(index, document, score)]``."""
|
|
177
|
+
from .inference.embedding import search as _search
|
|
178
|
+
|
|
179
|
+
return _search(model, query, documents, top_k=top_k, **kwargs)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def create_interface(model, model_type: str | None = None, **kwargs):
|
|
183
|
+
"""Generate and launch an interface for a trained model (gradio or terminal)."""
|
|
184
|
+
from .interface.generator import create_interface as _create
|
|
185
|
+
|
|
186
|
+
return _create(model, model_type=model_type, **kwargs)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def available_model_types() -> list:
|
|
190
|
+
"""Model types this build can train."""
|
|
191
|
+
from .core import registry
|
|
192
|
+
|
|
193
|
+
return registry.available()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
__all__ = [
|
|
197
|
+
"train",
|
|
198
|
+
"finetune",
|
|
199
|
+
"load_model",
|
|
200
|
+
"generate",
|
|
201
|
+
"embed",
|
|
202
|
+
"similarity",
|
|
203
|
+
"search",
|
|
204
|
+
"create_interface",
|
|
205
|
+
"available_model_types",
|
|
206
|
+
"AnthraciteTokenizer",
|
|
207
|
+
"ModelConfig",
|
|
208
|
+
"TrainingConfig",
|
|
209
|
+
"AnthraciteError",
|
|
210
|
+
"DatasetNotFoundError",
|
|
211
|
+
"UnsupportedDatasetError",
|
|
212
|
+
"UnsupportedModelTypeError",
|
|
213
|
+
"TokenizerError",
|
|
214
|
+
"ArchitectureError",
|
|
215
|
+
"DeviceError",
|
|
216
|
+
"TPUNotAvailableError",
|
|
217
|
+
"InsufficientMemoryError",
|
|
218
|
+
"InvalidConfigurationError",
|
|
219
|
+
"CheckpointError",
|
|
220
|
+
"GenerationError",
|
|
221
|
+
"__version__",
|
|
222
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .anthracite1_text import Anthracite1TextModel, build_text_model
|
|
2
|
+
from .anthracite1_embedding import Anthracite1EmbeddingModel, build_embedding_model
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"Anthracite1TextModel",
|
|
6
|
+
"Anthracite1EmbeddingModel",
|
|
7
|
+
"build_text_model",
|
|
8
|
+
"build_embedding_model",
|
|
9
|
+
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Anthracite-1 (embedding) – bidirectional encoder for sentence/document vectors.
|
|
2
|
+
|
|
3
|
+
Shares the Anthracite-1 building blocks with the text generator (RMSNorm,
|
|
4
|
+
rotary positions, SwiGLU) but attention is **bidirectional**, so every token
|
|
5
|
+
sees the whole sequence. Two training objectives are supported and both live
|
|
6
|
+
in this module:
|
|
7
|
+
|
|
8
|
+
* ``mlm`` – masked language modelling, for pretraining from raw text
|
|
9
|
+
* ``contrastive`` – in-batch-negative InfoNCE on (anchor, positive) pairs,
|
|
10
|
+
for turning a pretrained encoder into a retrieval model
|
|
11
|
+
|
|
12
|
+
A typical two-stage recipe is: ``train(..., objective="mlm")`` on a corpus,
|
|
13
|
+
then ``finetune(..., objective="contrastive")`` on pairs.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
|
|
20
|
+
import torch
|
|
21
|
+
import torch.nn as nn
|
|
22
|
+
import torch.nn.functional as F
|
|
23
|
+
|
|
24
|
+
from ..core.config import ModelConfig
|
|
25
|
+
from .anthracite1_text import RMSNorm, SwiGLU, apply_rope, build_rope_cache
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BidirectionalAttention(nn.Module):
|
|
29
|
+
"""Grouped-query attention without the causal mask."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: ModelConfig):
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.n_heads = config.n_heads
|
|
34
|
+
self.n_kv_heads = config.n_kv_heads
|
|
35
|
+
self.head_dim = config.d_model // config.n_heads
|
|
36
|
+
self.repeats = self.n_heads // self.n_kv_heads
|
|
37
|
+
self.dropout_p = config.dropout
|
|
38
|
+
|
|
39
|
+
self.wq = nn.Linear(config.d_model, self.n_heads * self.head_dim, bias=False)
|
|
40
|
+
self.wk = nn.Linear(config.d_model, self.n_kv_heads * self.head_dim, bias=False)
|
|
41
|
+
self.wv = nn.Linear(config.d_model, self.n_kv_heads * self.head_dim, bias=False)
|
|
42
|
+
self.wo = nn.Linear(self.n_heads * self.head_dim, config.d_model, bias=False)
|
|
43
|
+
|
|
44
|
+
def forward(self, x, cos, sin, key_padding_mask=None):
|
|
45
|
+
b, t, _ = x.shape
|
|
46
|
+
q = self.wq(x).view(b, t, self.n_heads, self.head_dim).transpose(1, 2)
|
|
47
|
+
k = self.wk(x).view(b, t, self.n_kv_heads, self.head_dim).transpose(1, 2)
|
|
48
|
+
v = self.wv(x).view(b, t, self.n_kv_heads, self.head_dim).transpose(1, 2)
|
|
49
|
+
|
|
50
|
+
q = apply_rope(q, cos, sin)
|
|
51
|
+
k = apply_rope(k, cos, sin)
|
|
52
|
+
if self.repeats > 1:
|
|
53
|
+
k = k.repeat_interleave(self.repeats, dim=1)
|
|
54
|
+
v = v.repeat_interleave(self.repeats, dim=1)
|
|
55
|
+
|
|
56
|
+
attn_mask = None
|
|
57
|
+
if key_padding_mask is not None:
|
|
58
|
+
# (B, T) True = real token → (B, 1, 1, T) broadcastable boolean mask
|
|
59
|
+
attn_mask = key_padding_mask[:, None, None, :].to(torch.bool)
|
|
60
|
+
|
|
61
|
+
if hasattr(F, "scaled_dot_product_attention"):
|
|
62
|
+
out = F.scaled_dot_product_attention(
|
|
63
|
+
q, k, v, attn_mask=attn_mask, dropout_p=self.dropout_p if self.training else 0.0
|
|
64
|
+
)
|
|
65
|
+
else: # pragma: no cover - very old torch
|
|
66
|
+
scores = (q @ k.transpose(-2, -1)) / math.sqrt(self.head_dim)
|
|
67
|
+
if attn_mask is not None:
|
|
68
|
+
scores = scores.masked_fill(~attn_mask, float("-inf"))
|
|
69
|
+
out = torch.softmax(scores.float(), dim=-1).to(q.dtype) @ v
|
|
70
|
+
|
|
71
|
+
out = out.transpose(1, 2).contiguous().view(b, t, -1)
|
|
72
|
+
return self.wo(out)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class EncoderBlock(nn.Module):
|
|
76
|
+
def __init__(self, config: ModelConfig):
|
|
77
|
+
super().__init__()
|
|
78
|
+
self.attn_norm = RMSNorm(config.d_model, config.norm_eps)
|
|
79
|
+
self.attn = BidirectionalAttention(config)
|
|
80
|
+
self.ffn_norm = RMSNorm(config.d_model, config.norm_eps)
|
|
81
|
+
self.ffn = SwiGLU(config)
|
|
82
|
+
self.dropout = nn.Dropout(config.dropout) if config.dropout > 0 else nn.Identity()
|
|
83
|
+
|
|
84
|
+
def forward(self, x, cos, sin, key_padding_mask=None):
|
|
85
|
+
x = x + self.dropout(self.attn(self.attn_norm(x), cos, sin, key_padding_mask))
|
|
86
|
+
x = x + self.dropout(self.ffn(self.ffn_norm(x)))
|
|
87
|
+
return x
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def masked_mean(hidden: torch.Tensor, mask: torch.Tensor) -> torch.Tensor:
|
|
91
|
+
weights = mask.unsqueeze(-1).to(hidden.dtype)
|
|
92
|
+
total = (hidden * weights).sum(dim=1)
|
|
93
|
+
count = weights.sum(dim=1).clamp(min=1e-6)
|
|
94
|
+
return total / count
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Anthracite1EmbeddingModel(nn.Module):
|
|
98
|
+
"""Bidirectional encoder producing fixed-size vectors."""
|
|
99
|
+
|
|
100
|
+
model_type = "embedding"
|
|
101
|
+
architecture = "anthracite-1"
|
|
102
|
+
|
|
103
|
+
def __init__(self, config: ModelConfig):
|
|
104
|
+
super().__init__()
|
|
105
|
+
config.validate()
|
|
106
|
+
self.config = config
|
|
107
|
+
self.token_embedding = nn.Embedding(config.vocab_size, config.d_model)
|
|
108
|
+
self.blocks = nn.ModuleList([EncoderBlock(config) for _ in range(config.n_layers)])
|
|
109
|
+
self.final_norm = RMSNorm(config.d_model, config.norm_eps)
|
|
110
|
+
|
|
111
|
+
self.projection = (
|
|
112
|
+
nn.Linear(config.d_model, config.embedding_dim, bias=False)
|
|
113
|
+
if config.embedding_dim and config.embedding_dim != config.d_model
|
|
114
|
+
else None
|
|
115
|
+
)
|
|
116
|
+
# MLM head: a small transform, output weights tied to the input embeddings
|
|
117
|
+
self.mlm_transform = nn.Linear(config.d_model, config.d_model, bias=False)
|
|
118
|
+
self.mlm_norm = RMSNorm(config.d_model, config.norm_eps)
|
|
119
|
+
|
|
120
|
+
self.head_dim = config.d_model // config.n_heads
|
|
121
|
+
self._rope_cache: tuple | None = None
|
|
122
|
+
self.apply(self._init_weights)
|
|
123
|
+
scale = (2 * config.n_layers) ** -0.5
|
|
124
|
+
for block in self.blocks:
|
|
125
|
+
nn.init.normal_(block.attn.wo.weight, mean=0.0, std=0.02 * scale)
|
|
126
|
+
nn.init.normal_(block.ffn.w2.weight, mean=0.0, std=0.02 * scale)
|
|
127
|
+
|
|
128
|
+
@staticmethod
|
|
129
|
+
def _init_weights(module: nn.Module) -> None:
|
|
130
|
+
if isinstance(module, nn.Linear):
|
|
131
|
+
nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
|
132
|
+
if module.bias is not None:
|
|
133
|
+
nn.init.zeros_(module.bias)
|
|
134
|
+
elif isinstance(module, nn.Embedding):
|
|
135
|
+
nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------ #
|
|
138
|
+
@property
|
|
139
|
+
def dimension(self) -> int:
|
|
140
|
+
return self.config.embedding_dim or self.config.d_model
|
|
141
|
+
|
|
142
|
+
def _rope(self, seq_len: int, device, dtype):
|
|
143
|
+
if self._rope_cache is None or self._rope_cache[0].size(0) < seq_len or self._rope_cache[0].device != device:
|
|
144
|
+
cos, sin = build_rope_cache(
|
|
145
|
+
max(seq_len, self.config.context_length), self.head_dim, self.config.rope_theta,
|
|
146
|
+
device, torch.float32,
|
|
147
|
+
)
|
|
148
|
+
self._rope_cache = (cos, sin)
|
|
149
|
+
cos, sin = self._rope_cache
|
|
150
|
+
return cos[:seq_len].to(dtype), sin[:seq_len].to(dtype)
|
|
151
|
+
|
|
152
|
+
def encode_hidden(self, input_ids, attention_mask=None):
|
|
153
|
+
b, t = input_ids.shape
|
|
154
|
+
if attention_mask is None:
|
|
155
|
+
attention_mask = input_ids.ne(self.config.pad_token_id)
|
|
156
|
+
x = self.token_embedding(input_ids)
|
|
157
|
+
cos, sin = self._rope(t, x.device, x.dtype)
|
|
158
|
+
for block in self.blocks:
|
|
159
|
+
x = block(x, cos, sin, attention_mask)
|
|
160
|
+
return self.final_norm(x), attention_mask
|
|
161
|
+
|
|
162
|
+
# -- forward variants ------------------------------------------------ #
|
|
163
|
+
def forward(self, input_ids, attention_mask=None, normalize: bool = True):
|
|
164
|
+
"""Return pooled sentence vectors."""
|
|
165
|
+
hidden, mask = self.encode_hidden(input_ids, attention_mask)
|
|
166
|
+
if self.config.pooling == "cls":
|
|
167
|
+
pooled = hidden[:, 0]
|
|
168
|
+
elif self.config.pooling == "max":
|
|
169
|
+
pooled = hidden.masked_fill(~mask.unsqueeze(-1), float("-inf")).max(dim=1).values
|
|
170
|
+
else:
|
|
171
|
+
pooled = masked_mean(hidden, mask)
|
|
172
|
+
if self.projection is not None:
|
|
173
|
+
pooled = self.projection(pooled)
|
|
174
|
+
if normalize:
|
|
175
|
+
pooled = F.normalize(pooled.float(), p=2, dim=-1).to(pooled.dtype)
|
|
176
|
+
return pooled
|
|
177
|
+
|
|
178
|
+
def mlm_logits(self, hidden: torch.Tensor) -> torch.Tensor:
|
|
179
|
+
h = self.mlm_norm(F.gelu(self.mlm_transform(hidden)))
|
|
180
|
+
return F.linear(h, self.token_embedding.weight)
|
|
181
|
+
|
|
182
|
+
# -- objectives ------------------------------------------------------ #
|
|
183
|
+
def mlm_loss(self, input_ids, labels, attention_mask=None):
|
|
184
|
+
hidden, _ = self.encode_hidden(input_ids, attention_mask)
|
|
185
|
+
logits = self.mlm_logits(hidden)
|
|
186
|
+
return F.cross_entropy(
|
|
187
|
+
logits.reshape(-1, logits.size(-1)).float(), labels.reshape(-1), ignore_index=-100
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def contrastive_loss(self, anchor_ids, positive_ids, anchor_mask=None, positive_mask=None,
|
|
191
|
+
negative_ids=None, negative_mask=None):
|
|
192
|
+
"""Symmetric InfoNCE with in-batch negatives (plus optional hard negatives)."""
|
|
193
|
+
a = self.forward(anchor_ids, anchor_mask, normalize=True).float()
|
|
194
|
+
p = self.forward(positive_ids, positive_mask, normalize=True).float()
|
|
195
|
+
bank = p
|
|
196
|
+
if negative_ids is not None:
|
|
197
|
+
n = self.forward(negative_ids, negative_mask, normalize=True).float()
|
|
198
|
+
bank = torch.cat([p, n], dim=0)
|
|
199
|
+
|
|
200
|
+
temperature = max(float(self.config.temperature), 1e-3)
|
|
201
|
+
logits = (a @ bank.t()) / temperature
|
|
202
|
+
target = torch.arange(a.size(0), device=a.device)
|
|
203
|
+
loss = F.cross_entropy(logits, target)
|
|
204
|
+
if negative_ids is None: # symmetric direction only makes sense without extra rows
|
|
205
|
+
loss = 0.5 * (loss + F.cross_entropy(logits.t(), target))
|
|
206
|
+
return loss
|
|
207
|
+
|
|
208
|
+
# -- convenience ----------------------------------------------------- #
|
|
209
|
+
@torch.no_grad()
|
|
210
|
+
def embed(self, input_ids, attention_mask=None, normalize: bool = True):
|
|
211
|
+
self.eval()
|
|
212
|
+
return self.forward(input_ids, attention_mask, normalize=normalize)
|
|
213
|
+
|
|
214
|
+
def num_parameters(self, trainable_only: bool = False) -> int:
|
|
215
|
+
return sum(p.numel() for p in self.parameters() if p.requires_grad or not trainable_only)
|
|
216
|
+
|
|
217
|
+
def resize_token_embeddings(self, new_vocab_size: int) -> None:
|
|
218
|
+
old = self.token_embedding
|
|
219
|
+
if new_vocab_size == old.num_embeddings:
|
|
220
|
+
return
|
|
221
|
+
new = nn.Embedding(new_vocab_size, self.config.d_model).to(old.weight.device, old.weight.dtype)
|
|
222
|
+
nn.init.normal_(new.weight, mean=0.0, std=0.02)
|
|
223
|
+
keep = min(new_vocab_size, old.num_embeddings)
|
|
224
|
+
with torch.no_grad():
|
|
225
|
+
new.weight[:keep] = old.weight[:keep]
|
|
226
|
+
self.token_embedding = new
|
|
227
|
+
self.config.vocab_size = new_vocab_size
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def build_embedding_model(config: ModelConfig) -> Anthracite1EmbeddingModel:
|
|
231
|
+
return Anthracite1EmbeddingModel(config)
|