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.
Files changed (53) hide show
  1. anthracite/__init__.py +222 -0
  2. anthracite/architectures/__init__.py +9 -0
  3. anthracite/architectures/anthracite1_embedding.py +231 -0
  4. anthracite/architectures/anthracite1_text.py +309 -0
  5. anthracite/cli.py +161 -0
  6. anthracite/core/__init__.py +4 -0
  7. anthracite/core/config.py +209 -0
  8. anthracite/core/exceptions.py +62 -0
  9. anthracite/core/finetuner.py +292 -0
  10. anthracite/core/registry.py +98 -0
  11. anthracite/core/trainer.py +487 -0
  12. anthracite/datasets/__init__.py +9 -0
  13. anthracite/datasets/hf.py +58 -0
  14. anthracite/datasets/loader.py +139 -0
  15. anthracite/datasets/local.py +235 -0
  16. anthracite/datasets/pairs.py +125 -0
  17. anthracite/datasets/text.py +142 -0
  18. anthracite/devices/__init__.py +3 -0
  19. anthracite/devices/auto.py +147 -0
  20. anthracite/devices/cpu.py +54 -0
  21. anthracite/devices/cuda.py +69 -0
  22. anthracite/devices/tpu.py +68 -0
  23. anthracite/inference/__init__.py +13 -0
  24. anthracite/inference/embedding.py +90 -0
  25. anthracite/inference/loader.py +86 -0
  26. anthracite/inference/text.py +64 -0
  27. anthracite/interface/__init__.py +3 -0
  28. anthracite/interface/generator.py +215 -0
  29. anthracite/io/__init__.py +4 -0
  30. anthracite/io/config.py +65 -0
  31. anthracite/io/metadata.py +157 -0
  32. anthracite/io/safetensors.py +147 -0
  33. anthracite/tokenizer/__init__.py +5 -0
  34. anthracite/tokenizer/builder.py +218 -0
  35. anthracite/tokenizer/tokenizer.py +230 -0
  36. anthracite/tokenizer/vocabulary.py +91 -0
  37. anthracite/training/__init__.py +18 -0
  38. anthracite/training/checkpoint.py +145 -0
  39. anthracite/training/loop.py +227 -0
  40. anthracite/training/memory.py +165 -0
  41. anthracite/training/optimizer.py +38 -0
  42. anthracite/training/precision.py +94 -0
  43. anthracite/training/scheduler.py +64 -0
  44. anthracite/utils/logging.py +105 -0
  45. anthracite/utils/parameters.py +187 -0
  46. anthracite/utils/progress.py +66 -0
  47. anthracite/utils/seed.py +76 -0
  48. anthracite-1.0.0.dist-info/METADATA +500 -0
  49. anthracite-1.0.0.dist-info/RECORD +53 -0
  50. anthracite-1.0.0.dist-info/WHEEL +5 -0
  51. anthracite-1.0.0.dist-info/entry_points.txt +2 -0
  52. anthracite-1.0.0.dist-info/licenses/LICENSE +15 -0
  53. 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)