sysone 0.2.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.
- sysone/__init__.py +22 -0
- sysone/_util.py +102 -0
- sysone/answers.py +57 -0
- sysone/architectures/__init__.py +12 -0
- sysone/architectures/base.py +144 -0
- sysone/architectures/modernbert.py +43 -0
- sysone/architectures/qwen.py +48 -0
- sysone/architectures/registry.py +53 -0
- sysone/base.py +890 -0
- sysone/cache.py +196 -0
- sysone/checkpoint.py +96 -0
- sysone/data.py +190 -0
- sysone/encoding.py +80 -0
- sysone/evaluation.py +167 -0
- sysone/features.py +98 -0
- sysone/heads.py +66 -0
- sysone/layout.py +129 -0
- sysone/losses.py +17 -0
- sysone/models.py +24 -0
- sysone/prompts.py +60 -0
- sysone/questions.py +186 -0
- sysone/training.py +399 -0
- sysone/tuning.py +210 -0
- sysone/version.py +1 -0
- sysone-0.2.0.dist-info/METADATA +144 -0
- sysone-0.2.0.dist-info/RECORD +28 -0
- sysone-0.2.0.dist-info/WHEEL +4 -0
- sysone-0.2.0.dist-info/licenses/LICENSE +21 -0
sysone/base.py
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
"""SystemOneModel: the scikit-learn style estimator behind EncoderModel and DecoderModel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import inspect
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import random
|
|
10
|
+
import shutil
|
|
11
|
+
import warnings
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from numbers import Real
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
|
|
19
|
+
from ._util import format_bytes, resolve_count, resolve_device, resolve_dtype
|
|
20
|
+
from .answers import answer
|
|
21
|
+
from .architectures.base import Architecture
|
|
22
|
+
from .architectures.registry import architecture_for, architecture_from_spec, architecture_spec
|
|
23
|
+
from .cache import FeatureCache, question_key
|
|
24
|
+
from .checkpoint import (
|
|
25
|
+
ADAPTER_DIR,
|
|
26
|
+
BACKBONE_FILE,
|
|
27
|
+
STATE_DIR,
|
|
28
|
+
load_adapter,
|
|
29
|
+
load_backbone_weights,
|
|
30
|
+
load_training_state,
|
|
31
|
+
save_adapter,
|
|
32
|
+
save_backbone_weights,
|
|
33
|
+
save_training_state,
|
|
34
|
+
)
|
|
35
|
+
from .data import Example, examples_from
|
|
36
|
+
from .encoding import Encoder, measure_limits
|
|
37
|
+
from .evaluation import score
|
|
38
|
+
from .features import cache_bytes, collate_features, extract
|
|
39
|
+
from .heads import CompactHead
|
|
40
|
+
from .layout import LAYOUTS, Encoded
|
|
41
|
+
from .losses import LOSSES
|
|
42
|
+
from .prompts import PromptFormat
|
|
43
|
+
from .questions import QUESTION_TYPES
|
|
44
|
+
from .training import (
|
|
45
|
+
CHECKS_PER_EPOCH,
|
|
46
|
+
OPTIMIZERS,
|
|
47
|
+
SCHEDULERS,
|
|
48
|
+
Network,
|
|
49
|
+
TrainItem,
|
|
50
|
+
TrainSettings,
|
|
51
|
+
TrainState,
|
|
52
|
+
evaluate_items,
|
|
53
|
+
sample_weights,
|
|
54
|
+
split_validation,
|
|
55
|
+
train,
|
|
56
|
+
)
|
|
57
|
+
from .tuning import (
|
|
58
|
+
LayerPlan,
|
|
59
|
+
add_adapters,
|
|
60
|
+
effective_tuning,
|
|
61
|
+
layer_plan,
|
|
62
|
+
lora_config,
|
|
63
|
+
quantization_config,
|
|
64
|
+
round_trained,
|
|
65
|
+
set_trainable,
|
|
66
|
+
trained_parameters,
|
|
67
|
+
wants_checkpointing,
|
|
68
|
+
)
|
|
69
|
+
from .version import __version__
|
|
70
|
+
|
|
71
|
+
TUNINGS = ("head", "qlora", "lora", "full")
|
|
72
|
+
HEADS = ("compact", "full")
|
|
73
|
+
LORA_TARGET_GROUPS = ("attention", "mlp", "all")
|
|
74
|
+
CONFIG_FILE = "config.json"
|
|
75
|
+
HEAD_FILE = "head.safetensors"
|
|
76
|
+
#: Above this estimated size, cached features are not kept in memory and the backbone runs every epoch.
|
|
77
|
+
CACHE_LIMIT_BYTES = 8 * 1024**3
|
|
78
|
+
#: With save(training_state="auto"), a larger training state is left out.
|
|
79
|
+
TRAINING_STATE_AUTO_BYTES = 100 * 1024**2
|
|
80
|
+
#: Examples per forward pass to start with when the backbone learns; halved if memory runs out.
|
|
81
|
+
START_MICRO_BATCH = 16
|
|
82
|
+
|
|
83
|
+
# Parameters that change the model's structure. If one changes between two calls to `fit`, the
|
|
84
|
+
# second call starts over instead of continuing training.
|
|
85
|
+
_STRUCTURAL = ("model_name", "revision", "architecture", "num_layers", "layout", "max_state_tokens",
|
|
86
|
+
"max_option_tokens", "max_len", "head", "head_layers", "head_dim", "head_dropout",
|
|
87
|
+
"precision", "device", "tuning", "trainable_layers", "lora_layers", "train_embeddings",
|
|
88
|
+
"lora_rank", "lora_alpha", "lora_dropout", "lora_targets", "quant_bits", "quant_type")
|
|
89
|
+
# Adapter settings that only matter for tuning="lora" / "qlora".
|
|
90
|
+
_ADAPTER_PARAMS = ("lora_rank", "lora_alpha", "lora_dropout", "lora_targets", "quant_bits", "quant_type")
|
|
91
|
+
# Settings that shape a training run. A saved run resumes only if these and the data are unchanged.
|
|
92
|
+
_RUN_PARAMS = ("batch_size", "validation_fraction", "validation_interval", "seed", "balance", "optimizer",
|
|
93
|
+
"scheduler", "warmup_ratio", "backbone_lr", "head_lr", "weight_decay", "grad_clip",
|
|
94
|
+
"shuffle_options", "label_smoothing")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class NotFittedError(RuntimeError):
|
|
98
|
+
"""Raised when predicting with a model that has neither been fit nor loaded."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class SystemOneModel:
|
|
102
|
+
"""Fine-tune and calibrate a system-one decision model.
|
|
103
|
+
|
|
104
|
+
Every hyperparameter is a keyword argument; `__init__` only stores them, so `get_params`,
|
|
105
|
+
`set_params` and `clone`-style copying work. Anything learned in `fit` is stored on an
|
|
106
|
+
attribute with a trailing underscore. See `docs/DESIGN.md` for every parameter.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
model_name: str,
|
|
112
|
+
*,
|
|
113
|
+
architecture: Architecture | None = None,
|
|
114
|
+
revision: str | None = None,
|
|
115
|
+
# how much of the backbone learns
|
|
116
|
+
tuning: str = "qlora",
|
|
117
|
+
num_layers=None,
|
|
118
|
+
trainable_layers=None,
|
|
119
|
+
lora_layers=None,
|
|
120
|
+
train_embeddings: bool = False,
|
|
121
|
+
layout: str = "parallel",
|
|
122
|
+
# length limits
|
|
123
|
+
max_state_tokens="auto",
|
|
124
|
+
max_option_tokens="auto",
|
|
125
|
+
max_len: int | None = None,
|
|
126
|
+
# adapter details
|
|
127
|
+
lora_rank: int = 8,
|
|
128
|
+
lora_alpha: int | None = None,
|
|
129
|
+
lora_dropout: float = 0.0,
|
|
130
|
+
lora_targets="attention",
|
|
131
|
+
quant_bits: int = 4,
|
|
132
|
+
quant_type: str = "nf4",
|
|
133
|
+
# decision head
|
|
134
|
+
head: str = "compact",
|
|
135
|
+
head_layers: int = 2,
|
|
136
|
+
head_dim="auto",
|
|
137
|
+
head_dropout: float = 0.0,
|
|
138
|
+
# optimization
|
|
139
|
+
optimizer: str = "adamw",
|
|
140
|
+
backbone_lr="auto",
|
|
141
|
+
head_lr: float = 1e-3,
|
|
142
|
+
weight_decay: float = 0.01,
|
|
143
|
+
scheduler: str = "cosine",
|
|
144
|
+
warmup_ratio: float = 0.05,
|
|
145
|
+
grad_clip: float = 1.0,
|
|
146
|
+
batch_size: int = 64,
|
|
147
|
+
epochs: int = 20,
|
|
148
|
+
# stopping and validation
|
|
149
|
+
early_stopping: bool = True,
|
|
150
|
+
validation_fraction: float = 0.1,
|
|
151
|
+
n_iter_no_change: int = 3,
|
|
152
|
+
target_loss: float | None = None,
|
|
153
|
+
validation_interval="auto",
|
|
154
|
+
# objective and data
|
|
155
|
+
loss: str = "soft-ce",
|
|
156
|
+
label_smoothing: float = 0.0,
|
|
157
|
+
shuffle_options="auto",
|
|
158
|
+
balance: str | None = None,
|
|
159
|
+
calibrate: bool = True,
|
|
160
|
+
# runtime
|
|
161
|
+
cache_features="auto",
|
|
162
|
+
cache_dir=None,
|
|
163
|
+
cache_max_gb: float = 5,
|
|
164
|
+
precision: str = "auto",
|
|
165
|
+
gradient_checkpointing="auto",
|
|
166
|
+
device: str = "auto",
|
|
167
|
+
seed: int = 0,
|
|
168
|
+
verbose: int = 1,
|
|
169
|
+
):
|
|
170
|
+
self.model_name = model_name
|
|
171
|
+
self.architecture = architecture
|
|
172
|
+
self.revision = revision
|
|
173
|
+
self.tuning = tuning
|
|
174
|
+
self.num_layers = num_layers
|
|
175
|
+
self.trainable_layers = trainable_layers
|
|
176
|
+
self.lora_layers = lora_layers
|
|
177
|
+
self.train_embeddings = train_embeddings
|
|
178
|
+
self.layout = layout
|
|
179
|
+
self.max_state_tokens = max_state_tokens
|
|
180
|
+
self.max_option_tokens = max_option_tokens
|
|
181
|
+
self.max_len = max_len
|
|
182
|
+
self.lora_rank = lora_rank
|
|
183
|
+
self.lora_alpha = lora_alpha
|
|
184
|
+
self.lora_dropout = lora_dropout
|
|
185
|
+
self.lora_targets = lora_targets
|
|
186
|
+
self.quant_bits = quant_bits
|
|
187
|
+
self.quant_type = quant_type
|
|
188
|
+
self.head = head
|
|
189
|
+
self.head_layers = head_layers
|
|
190
|
+
self.head_dim = head_dim
|
|
191
|
+
self.head_dropout = head_dropout
|
|
192
|
+
self.optimizer = optimizer
|
|
193
|
+
self.backbone_lr = backbone_lr
|
|
194
|
+
self.head_lr = head_lr
|
|
195
|
+
self.weight_decay = weight_decay
|
|
196
|
+
self.scheduler = scheduler
|
|
197
|
+
self.warmup_ratio = warmup_ratio
|
|
198
|
+
self.grad_clip = grad_clip
|
|
199
|
+
self.batch_size = batch_size
|
|
200
|
+
self.epochs = epochs
|
|
201
|
+
self.early_stopping = early_stopping
|
|
202
|
+
self.validation_fraction = validation_fraction
|
|
203
|
+
self.n_iter_no_change = n_iter_no_change
|
|
204
|
+
self.target_loss = target_loss
|
|
205
|
+
self.validation_interval = validation_interval
|
|
206
|
+
self.loss = loss
|
|
207
|
+
self.label_smoothing = label_smoothing
|
|
208
|
+
self.shuffle_options = shuffle_options
|
|
209
|
+
self.balance = balance
|
|
210
|
+
self.calibrate = calibrate
|
|
211
|
+
self.cache_features = cache_features
|
|
212
|
+
self.cache_dir = cache_dir
|
|
213
|
+
self.cache_max_gb = cache_max_gb
|
|
214
|
+
self.precision = precision
|
|
215
|
+
self.gradient_checkpointing = gradient_checkpointing
|
|
216
|
+
self.device = device
|
|
217
|
+
self.seed = seed
|
|
218
|
+
self.verbose = verbose
|
|
219
|
+
|
|
220
|
+
# -- parameters ----------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
@classmethod
|
|
223
|
+
def _param_defaults(cls) -> dict[str, Any]:
|
|
224
|
+
"""Parameter names and defaults, read from the `__init__` signatures along the class
|
|
225
|
+
hierarchy. A subclass's default wins over its base's."""
|
|
226
|
+
defaults: dict[str, Any] = {}
|
|
227
|
+
for klass in reversed(cls.__mro__):
|
|
228
|
+
init = klass.__dict__.get("__init__")
|
|
229
|
+
if init is None or klass is object:
|
|
230
|
+
continue
|
|
231
|
+
for name, p in inspect.signature(init).parameters.items():
|
|
232
|
+
if name == "self" or p.kind in (p.VAR_KEYWORD, p.VAR_POSITIONAL):
|
|
233
|
+
continue
|
|
234
|
+
defaults[name] = p.default
|
|
235
|
+
return defaults
|
|
236
|
+
|
|
237
|
+
def get_params(self, deep: bool = True) -> dict[str, Any]:
|
|
238
|
+
return {name: getattr(self, name) for name in self._param_defaults()}
|
|
239
|
+
|
|
240
|
+
def set_params(self, **params) -> SystemOneModel:
|
|
241
|
+
valid = self._param_defaults()
|
|
242
|
+
for name, value in params.items():
|
|
243
|
+
if name not in valid:
|
|
244
|
+
raise ValueError("invalid parameter %r for %s; valid parameters: %s"
|
|
245
|
+
% (name, type(self).__name__, ", ".join(sorted(valid))))
|
|
246
|
+
setattr(self, name, value)
|
|
247
|
+
return self
|
|
248
|
+
|
|
249
|
+
def __repr__(self) -> str:
|
|
250
|
+
defaults = self._param_defaults()
|
|
251
|
+
changed = ["%s=%r" % (k, v) for k, v in self.get_params().items()
|
|
252
|
+
if defaults.get(k, inspect.Parameter.empty) is inspect.Parameter.empty or v != defaults[k]]
|
|
253
|
+
return "%s(%s)" % (type(self).__name__, ", ".join(changed))
|
|
254
|
+
|
|
255
|
+
def _log(self, message: str) -> None:
|
|
256
|
+
if self.verbose:
|
|
257
|
+
print("[sysone] %s" % message, flush=True)
|
|
258
|
+
|
|
259
|
+
# -- validation ----------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def _check_params(self) -> None:
|
|
262
|
+
"""Reject unsupported values before any weights load."""
|
|
263
|
+
|
|
264
|
+
def one_of(name, allowed):
|
|
265
|
+
value = getattr(self, name)
|
|
266
|
+
if value not in allowed:
|
|
267
|
+
raise ValueError("%s must be one of %s, got %r" % (name, ", ".join(map(repr, allowed)), value))
|
|
268
|
+
|
|
269
|
+
def positive_int(name):
|
|
270
|
+
value = getattr(self, name)
|
|
271
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 1:
|
|
272
|
+
raise ValueError("%s must be a positive integer, got %r" % (name, value))
|
|
273
|
+
|
|
274
|
+
def positive_number(name, optional=False):
|
|
275
|
+
value = getattr(self, name)
|
|
276
|
+
if optional and value is None:
|
|
277
|
+
return
|
|
278
|
+
if isinstance(value, bool) or not isinstance(value, Real) or not value > 0 or not math.isfinite(value):
|
|
279
|
+
raise ValueError("%s must be a positive number%s, got %r"
|
|
280
|
+
% (name, " or None" if optional else "", value))
|
|
281
|
+
|
|
282
|
+
one_of("tuning", TUNINGS)
|
|
283
|
+
one_of("layout", LAYOUTS)
|
|
284
|
+
one_of("head", HEADS)
|
|
285
|
+
one_of("optimizer", OPTIMIZERS)
|
|
286
|
+
one_of("scheduler", SCHEDULERS)
|
|
287
|
+
one_of("balance", (None, "category", "question"))
|
|
288
|
+
one_of("quant_bits", (4, 8))
|
|
289
|
+
one_of("quant_type", ("nf4", "fp4"))
|
|
290
|
+
one_of("gradient_checkpointing", ("auto", True, False))
|
|
291
|
+
for name in ("batch_size", "epochs", "n_iter_no_change", "lora_rank"):
|
|
292
|
+
positive_int(name)
|
|
293
|
+
positive_number("target_loss", optional=True)
|
|
294
|
+
positive_number("lora_alpha", optional=True)
|
|
295
|
+
positive_number("cache_max_gb")
|
|
296
|
+
if self.backbone_lr != "auto":
|
|
297
|
+
positive_number("backbone_lr")
|
|
298
|
+
if self.validation_interval != "auto":
|
|
299
|
+
positive_int("validation_interval")
|
|
300
|
+
if not 0.0 <= self.lora_dropout < 1.0:
|
|
301
|
+
raise ValueError("lora_dropout must be in [0, 1), got %r" % (self.lora_dropout,))
|
|
302
|
+
targets = self.lora_targets
|
|
303
|
+
if isinstance(targets, str) and targets not in LORA_TARGET_GROUPS or not isinstance(targets, str) and (
|
|
304
|
+
not isinstance(targets, (list, tuple)) or not targets or not all(isinstance(t, str) for t in targets)):
|
|
305
|
+
raise ValueError("lora_targets must be one of %s or a list of module names, got %r"
|
|
306
|
+
% (", ".join(map(repr, LORA_TARGET_GROUPS)), targets))
|
|
307
|
+
if isinstance(self.head_layers, bool) or not isinstance(self.head_layers, int) or self.head_layers < 0:
|
|
308
|
+
raise ValueError("head_layers must be a non-negative integer, got %r" % (self.head_layers,))
|
|
309
|
+
if not 0.0 <= self.validation_fraction < 1.0:
|
|
310
|
+
raise ValueError("validation_fraction must be in [0, 1), got %r" % (self.validation_fraction,))
|
|
311
|
+
if not 0.0 <= self.label_smoothing < 1.0:
|
|
312
|
+
raise ValueError("label_smoothing must be in [0, 1), got %r" % (self.label_smoothing,))
|
|
313
|
+
for name in ("max_state_tokens", "max_option_tokens"):
|
|
314
|
+
value = getattr(self, name)
|
|
315
|
+
if value != "auto" and (isinstance(value, bool) or not isinstance(value, int) or value < 1):
|
|
316
|
+
raise ValueError("%s must be 'auto' or a positive integer, got %r" % (name, value))
|
|
317
|
+
if self.head_dim != "auto" and (isinstance(self.head_dim, bool) or not isinstance(self.head_dim, int)
|
|
318
|
+
or self.head_dim < 1):
|
|
319
|
+
raise ValueError("head_dim must be 'auto' or a positive integer, got %r" % (self.head_dim,))
|
|
320
|
+
if self.shuffle_options not in ("auto", None):
|
|
321
|
+
unknown = set(self.shuffle_options) - set(QUESTION_TYPES)
|
|
322
|
+
if isinstance(self.shuffle_options, str) or unknown:
|
|
323
|
+
raise ValueError("shuffle_options must be 'auto', None or a list of question types, got %r"
|
|
324
|
+
% (self.shuffle_options,))
|
|
325
|
+
if self.loss not in LOSSES + ("rlcd",):
|
|
326
|
+
raise ValueError("loss must be one of 'soft-ce', 'rlcd', got %r" % (self.loss,))
|
|
327
|
+
if self.cache_features not in ("auto", True, False):
|
|
328
|
+
raise ValueError("cache_features must be 'auto', True or False, got %r" % (self.cache_features,))
|
|
329
|
+
if self.tuning in ("head", "full") and self.lora_layers not in (None, 0):
|
|
330
|
+
raise ValueError("lora_layers=%r needs tuning='lora' or 'qlora', not %r" % (self.lora_layers, self.tuning))
|
|
331
|
+
if self.tuning == "qlora" and self.trainable_layers not in (None, 0):
|
|
332
|
+
raise ValueError("tuning='qlora' can't fully train layers (trainable_layers=%r): quantized layers "
|
|
333
|
+
"can't be fine-tuned; use tuning='lora'" % (self.trainable_layers,))
|
|
334
|
+
if self.tuning == "head" and (self.trainable_layers not in (None, 0) or self.train_embeddings):
|
|
335
|
+
raise ValueError("tuning='head' trains only the head; trainable_layers and train_embeddings need "
|
|
336
|
+
"tuning='lora', 'qlora' or 'full'")
|
|
337
|
+
if self.tuning != "head" and self.cache_features is True:
|
|
338
|
+
raise ValueError("cache_features=True needs tuning='head': features can only be cached when the "
|
|
339
|
+
"backbone is frozen")
|
|
340
|
+
if self.tuning not in ("lora", "qlora"):
|
|
341
|
+
defaults = self._param_defaults()
|
|
342
|
+
changed = [n for n in _ADAPTER_PARAMS if getattr(self, n) != defaults[n]]
|
|
343
|
+
if changed:
|
|
344
|
+
warnings.warn("%s only apply to tuning='lora' or 'qlora' and are ignored with tuning=%r"
|
|
345
|
+
% (", ".join(changed), self.tuning), UserWarning, stacklevel=3)
|
|
346
|
+
if self.cache_dir is not None and self.tuning != "head":
|
|
347
|
+
warnings.warn("cache_dir is ignored with tuning=%r: features can only be cached when the backbone "
|
|
348
|
+
"is frozen (tuning='head')" % (self.tuning,), UserWarning, stacklevel=3)
|
|
349
|
+
|
|
350
|
+
# Not implemented yet in this version.
|
|
351
|
+
if self.head != "compact":
|
|
352
|
+
raise NotImplementedError("head=%r is not implemented yet; use head='compact'" % (self.head,))
|
|
353
|
+
if self.loss != "soft-ce":
|
|
354
|
+
raise NotImplementedError("loss=%r is not implemented yet; use loss='soft-ce'" % (self.loss,))
|
|
355
|
+
|
|
356
|
+
# -- building ------------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def is_fitted(self) -> bool:
|
|
360
|
+
return getattr(self, "head_", None) is not None
|
|
361
|
+
|
|
362
|
+
def _structure(self) -> dict:
|
|
363
|
+
params = self.get_params()
|
|
364
|
+
params["architecture"] = None if self.architecture is None else architecture_spec(self.architecture)
|
|
365
|
+
return {k: params[k] for k in _STRUCTURAL}
|
|
366
|
+
|
|
367
|
+
def _shuffle_types(self) -> list[str]:
|
|
368
|
+
if self.shuffle_options == "auto":
|
|
369
|
+
return [] if self.layout == "parallel" else ["choice"]
|
|
370
|
+
return list(self.shuffle_options or [])
|
|
371
|
+
|
|
372
|
+
def _load_backbone(self, num_layers, revision, tuning: str) -> None:
|
|
373
|
+
"""Load the tokenizer and the backbone (in 4-bit or 8-bit for tuning="qlora")."""
|
|
374
|
+
from transformers import AutoTokenizer
|
|
375
|
+
|
|
376
|
+
self.device_ = resolve_device(self.device)
|
|
377
|
+
self.dtype_ = resolve_dtype(self.precision, self.device_)
|
|
378
|
+
self.tuning_ = tuning
|
|
379
|
+
self.architecture_ = self.architecture if self.architecture is not None else \
|
|
380
|
+
architecture_for(self.model_name, revision=revision)
|
|
381
|
+
self.tokenizer_ = AutoTokenizer.from_pretrained(self.model_name, revision=revision)
|
|
382
|
+
self.architecture_.check_tokenizer(self.tokenizer_)
|
|
383
|
+
quantization = None
|
|
384
|
+
if tuning == "qlora":
|
|
385
|
+
compute = self.dtype_ if self.dtype_ != torch.float32 else torch.bfloat16
|
|
386
|
+
quantization = quantization_config(self.quant_bits, self.quant_type, compute)
|
|
387
|
+
backbone = self.architecture_.load(self.model_name, num_layers, self.dtype_, revision=revision,
|
|
388
|
+
quantization=quantization, device=self.device_)
|
|
389
|
+
backbone.requires_grad_(False)
|
|
390
|
+
if quantization is None:
|
|
391
|
+
backbone = backbone.to(self.device_)
|
|
392
|
+
self.backbone_ = backbone.eval()
|
|
393
|
+
self.base_revision_ = revision or getattr(backbone.config, "_commit_hash", None)
|
|
394
|
+
model_limit = self.architecture_.max_positions(backbone)
|
|
395
|
+
self.max_len_ = min(self.max_len, model_limit) if self.max_len else model_limit
|
|
396
|
+
|
|
397
|
+
def _plan(self, tuning: str) -> LayerPlan:
|
|
398
|
+
"""The layer plan, worked out from the model's config before any weights load."""
|
|
399
|
+
from transformers import AutoConfig
|
|
400
|
+
|
|
401
|
+
total = AutoConfig.from_pretrained(self.model_name, revision=self.revision).num_hidden_layers
|
|
402
|
+
kept = resolve_count(self.num_layers, total, "num_layers")
|
|
403
|
+
return layer_plan(tuning, kept, self.trainable_layers, self.lora_layers, self.train_embeddings)
|
|
404
|
+
|
|
405
|
+
def _setup(self, examples: list[Example]) -> None:
|
|
406
|
+
tuning = effective_tuning(self.tuning, resolve_device(self.device), stacklevel=3)
|
|
407
|
+
self.plan_ = self._plan(tuning)
|
|
408
|
+
self._load_backbone(self.num_layers, self.revision, tuning)
|
|
409
|
+
if self.plan_.n_lora:
|
|
410
|
+
add_adapters(self.backbone_, lora_config(self.architecture_, self.backbone_, self.plan_, self.lora_targets,
|
|
411
|
+
self.lora_rank, self.lora_alpha, self.lora_dropout))
|
|
412
|
+
self.prompt_format_ = PromptFormat()
|
|
413
|
+
probe = Encoder(self.architecture_, self.tokenizer_, self.prompt_format_, self.layout,
|
|
414
|
+
1, 1, self.max_len_)
|
|
415
|
+
self.max_state_tokens_, self.max_option_tokens_ = measure_limits(
|
|
416
|
+
probe, [ex.state for ex in examples], [ex.question for ex in examples],
|
|
417
|
+
self.max_state_tokens, self.max_option_tokens)
|
|
418
|
+
hidden = self.backbone_.config.hidden_size
|
|
419
|
+
self.head_dim_ = max(1, hidden // 2) if self.head_dim == "auto" else self.head_dim
|
|
420
|
+
self.head_ = CompactHead(hidden, self.head_dim_, self.head_layers, self.head_dropout).to(self.device_)
|
|
421
|
+
self.temperatures_ = {t: 1.0 for t in QUESTION_TYPES}
|
|
422
|
+
self.history_ = []
|
|
423
|
+
self.training_state_ = None
|
|
424
|
+
self.structure_ = self._structure()
|
|
425
|
+
plan = self.plan_
|
|
426
|
+
trains = "head only" if not plan.learns else ", ".join(part for part in (
|
|
427
|
+
"%d fully trained layers" % plan.n_full if plan.n_full else "",
|
|
428
|
+
"adapters on %d layers" % plan.n_lora if plan.n_lora else "",
|
|
429
|
+
"embeddings" if plan.train_embeddings else "") if part)
|
|
430
|
+
self._log("loaded %s (%s, %d of %d layers, %s on %s); tuning=%s: %s; max_state_tokens=%d, "
|
|
431
|
+
"max_option_tokens=%d"
|
|
432
|
+
% (self.model_name, self.architecture_.name(), self.backbone_.config.num_hidden_layers,
|
|
433
|
+
self.backbone_.sysone_total_layers, str(self.dtype_).replace("torch.", ""),
|
|
434
|
+
self.device_, self.tuning_, trains, self.max_state_tokens_, self.max_option_tokens_))
|
|
435
|
+
|
|
436
|
+
def _learning_rates(self) -> dict[str, float]:
|
|
437
|
+
"""Learning rate per parameter group. `backbone_lr="auto"` takes the architecture's defaults
|
|
438
|
+
for fully trained layers and for adapters; a number sets both."""
|
|
439
|
+
rates = dict(self.architecture_.learning_rates)
|
|
440
|
+
if self.backbone_lr != "auto":
|
|
441
|
+
rates = {group: float(self.backbone_lr) for group in rates}
|
|
442
|
+
return {"head": self.head_lr, **rates}
|
|
443
|
+
|
|
444
|
+
def _autocast_dtype(self) -> torch.dtype | None:
|
|
445
|
+
"""Trained weights are fp32 next to half-precision frozen ones, so the backbone runs under
|
|
446
|
+
autocast when anything in it trains."""
|
|
447
|
+
if self.plan_.learns and self.dtype_ != torch.float32:
|
|
448
|
+
return self.dtype_
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
def _fingerprint(self, examples: list[Example]) -> str:
|
|
452
|
+
"""Identifies the training data and the settings that shape a run, so a saved training
|
|
453
|
+
state is only resumed on the same run."""
|
|
454
|
+
h = hashlib.sha256()
|
|
455
|
+
for ex in examples:
|
|
456
|
+
q = ex.question
|
|
457
|
+
h.update(json.dumps([ex.state, ex.qid, q.type, q.instructions, list(q.keys), list(q.texts), ex.target,
|
|
458
|
+
ex.category], default=str).encode())
|
|
459
|
+
params = self.get_params()
|
|
460
|
+
h.update(json.dumps({k: params[k] for k in _RUN_PARAMS}, sort_keys=True, default=str).encode())
|
|
461
|
+
return h.hexdigest()
|
|
462
|
+
|
|
463
|
+
def _disk_cache(self) -> FeatureCache:
|
|
464
|
+
parts = {
|
|
465
|
+
"model_name": self.model_name, "revision": self.base_revision_,
|
|
466
|
+
"architecture": architecture_spec(self.architecture_),
|
|
467
|
+
"num_layers": self.backbone_.config.num_hidden_layers, "layout": self.layout,
|
|
468
|
+
"precision": str(self.dtype_), "prompt_format": self.prompt_format_.to_dict(),
|
|
469
|
+
"max_state_tokens": self.max_state_tokens_, "max_option_tokens": self.max_option_tokens_,
|
|
470
|
+
"max_len": self.max_len_,
|
|
471
|
+
}
|
|
472
|
+
return FeatureCache(self.cache_dir, parts, self.cache_max_gb * 1024**3, self._log)
|
|
473
|
+
|
|
474
|
+
def _features(self, encoded: list[Encoded], disk: FeatureCache | None):
|
|
475
|
+
"""Frozen-backbone vectors for `encoded`, read from and added to the disk cache if given."""
|
|
476
|
+
def compute(items):
|
|
477
|
+
return extract(self.architecture_, self.backbone_, items, self.tokenizer_.pad_token_id, self.device_)
|
|
478
|
+
|
|
479
|
+
if disk is None:
|
|
480
|
+
return compute(encoded)
|
|
481
|
+
keys = [question_key(e) for e in encoded]
|
|
482
|
+
found = disk.lookup(keys)
|
|
483
|
+
todo: dict[bytes, int] = {}
|
|
484
|
+
for i, key in enumerate(keys):
|
|
485
|
+
if key not in found and key not in todo:
|
|
486
|
+
todo[key] = i
|
|
487
|
+
computed = compute([encoded[i] for i in todo.values()]) if todo else []
|
|
488
|
+
found.update(zip(todo, computed))
|
|
489
|
+
written = disk.add(list(todo), computed)
|
|
490
|
+
self._log("feature cache %s: %d of %d questions read from disk, %d computed; wrote %s, folder total %s"
|
|
491
|
+
% (disk.path, len(keys) - len(todo), len(keys), len(todo), format_bytes(written),
|
|
492
|
+
format_bytes(disk.folder_bytes())))
|
|
493
|
+
return [found[k] for k in keys]
|
|
494
|
+
|
|
495
|
+
def _encoder(self) -> Encoder:
|
|
496
|
+
return Encoder(self.architecture_, self.tokenizer_, self.prompt_format_, self.layout,
|
|
497
|
+
self.max_state_tokens_, self.max_option_tokens_, self.max_len_)
|
|
498
|
+
|
|
499
|
+
# -- fit -----------------------------------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
def fit(self, data, questions: Mapping | None = None) -> SystemOneModel:
|
|
502
|
+
"""Train on labelled rows. See `sysone.data` for the accepted input forms.
|
|
503
|
+
|
|
504
|
+
Calling `fit` again continues training the same model, unless a structural parameter
|
|
505
|
+
(the base model, layers, layout, head shape, token limits, tuning mode, ...) changed, in
|
|
506
|
+
which case it starts over. With a training state from the last run (kept in memory, or
|
|
507
|
+
saved with the model) and the same data and run settings, it resumes that run exactly;
|
|
508
|
+
`epochs` counts the total. Otherwise it warm-starts: the trained weights are kept and a new
|
|
509
|
+
optimizer and schedule train `epochs` more epochs.
|
|
510
|
+
|
|
511
|
+
Interrupting `fit` (Ctrl-C) keeps the best weights so far and the state to resume from.
|
|
512
|
+
"""
|
|
513
|
+
self._check_params()
|
|
514
|
+
examples, skipped = examples_from(data, questions, labeled=True, label_smoothing=self.label_smoothing)
|
|
515
|
+
if not examples:
|
|
516
|
+
raise ValueError("the data produced no labelled questions (skipped: %s)" % dict(skipped))
|
|
517
|
+
if skipped:
|
|
518
|
+
self._log("skipped %s" % ", ".join("%d %s" % (n, why) for why, n in skipped.items()))
|
|
519
|
+
torch.manual_seed(self.seed)
|
|
520
|
+
|
|
521
|
+
if self.is_fitted and self.structure_ != self._structure():
|
|
522
|
+
self._log("structural parameters changed since the last fit; starting over")
|
|
523
|
+
self.head_ = None
|
|
524
|
+
fresh = not self.is_fitted
|
|
525
|
+
if fresh:
|
|
526
|
+
self._setup(examples)
|
|
527
|
+
if self.calibrate:
|
|
528
|
+
self._log("calibration is not implemented yet; temperatures stay at 1.0")
|
|
529
|
+
plan = self.plan_
|
|
530
|
+
backbone_groups = set_trainable(self.architecture_, self.backbone_, plan)
|
|
531
|
+
|
|
532
|
+
train_idx, val_idx = split_validation(examples, self.validation_fraction, self.seed)
|
|
533
|
+
train_ex = [examples[i] for i in train_idx]
|
|
534
|
+
val_ex = [examples[i] for i in val_idx]
|
|
535
|
+
self._log("%d training questions, %d validation questions" % (len(train_ex), len(val_ex)))
|
|
536
|
+
if self.early_stopping and not val_ex:
|
|
537
|
+
self._log("too little data for a validation slice; early stopping is off")
|
|
538
|
+
|
|
539
|
+
encoder = self._encoder()
|
|
540
|
+
shuffle = set(self._shuffle_types())
|
|
541
|
+
hidden = self.backbone_.config.hidden_size
|
|
542
|
+
n_vectors = sum(ex.question.k + 1 for ex in examples)
|
|
543
|
+
cache = self.cache_features
|
|
544
|
+
if plan.learns:
|
|
545
|
+
cache = False
|
|
546
|
+
elif cache == "auto":
|
|
547
|
+
cache = not shuffle and cache_bytes(n_vectors, hidden) <= CACHE_LIMIT_BYTES
|
|
548
|
+
elif cache and shuffle:
|
|
549
|
+
raise ValueError("cache_features=True can't be used while options are shuffled (shuffle_options=%r)"
|
|
550
|
+
% (self.shuffle_options,))
|
|
551
|
+
disk = None
|
|
552
|
+
if self.cache_dir is not None and not plan.learns:
|
|
553
|
+
if cache:
|
|
554
|
+
disk = self._disk_cache()
|
|
555
|
+
else:
|
|
556
|
+
self._log("cache_dir is unused: features aren't cached (%s)"
|
|
557
|
+
% ("options are shuffled" if shuffle else "cache_features=%r" % self.cache_features))
|
|
558
|
+
|
|
559
|
+
def items_for(exs: list[Example], rng: random.Random | None) -> list[TrainItem]:
|
|
560
|
+
encoded, items_meta = [], []
|
|
561
|
+
for ex in exs:
|
|
562
|
+
order = None
|
|
563
|
+
if rng is not None and ex.question.type in shuffle and ex.question.k > 1:
|
|
564
|
+
order = list(range(ex.question.k))
|
|
565
|
+
rng.shuffle(order)
|
|
566
|
+
target = ex.target if order is None else [ex.target[i] for i in order]
|
|
567
|
+
encoded.append(encoder.encode(ex.state, ex.question, order))
|
|
568
|
+
items_meta.append((ex.question.type_index, torch.tensor(target, dtype=torch.float32)))
|
|
569
|
+
inputs = encoded if plan.learns else self._features(encoded, disk if rng is None else None)
|
|
570
|
+
return [TrainItem(x, qtype, target) for x, (qtype, target) in zip(inputs, items_meta)]
|
|
571
|
+
|
|
572
|
+
val_items = items_for(val_ex, None)
|
|
573
|
+
reuse = cache or (plan.learns and not shuffle)
|
|
574
|
+
cached: list[TrainItem] | None = None
|
|
575
|
+
|
|
576
|
+
def train_items(epoch: int) -> list[TrainItem]:
|
|
577
|
+
nonlocal cached
|
|
578
|
+
if reuse:
|
|
579
|
+
if cached is None:
|
|
580
|
+
if not plan.learns:
|
|
581
|
+
self._log("computing backbone features once for %d questions" % len(train_ex))
|
|
582
|
+
cached = items_for(train_ex, None)
|
|
583
|
+
return cached
|
|
584
|
+
return items_for(train_ex, random.Random(self.seed + epoch) if shuffle else None)
|
|
585
|
+
|
|
586
|
+
micro = START_MICRO_BATCH if plan.learns else self.batch_size
|
|
587
|
+
if plan.learns:
|
|
588
|
+
rates = self._learning_rates()
|
|
589
|
+
self._log("learning rates: head %g, %s" % (rates["head"], ", ".join(
|
|
590
|
+
"%s %g" % ("adapters" if group == "adapter" else "fully trained layers", rates[group])
|
|
591
|
+
for group, named in backbone_groups.items() if named)))
|
|
592
|
+
lengths = sorted(len(encoder.encode(ex.state, ex.question).input_ids) for ex in train_ex)
|
|
593
|
+
seq_len = lengths[min(len(lengths) - 1, int(0.95 * len(lengths)))]
|
|
594
|
+
checkpointing = wants_checkpointing(self.gradient_checkpointing, plan, self.backbone_, seq_len,
|
|
595
|
+
min(micro, self.batch_size))
|
|
596
|
+
if checkpointing:
|
|
597
|
+
self.backbone_.gradient_checkpointing_enable(gradient_checkpointing_kwargs={"use_reentrant": False})
|
|
598
|
+
self._log("gradient checkpointing is on")
|
|
599
|
+
elif self.backbone_.is_gradient_checkpointing:
|
|
600
|
+
self.backbone_.gradient_checkpointing_disable()
|
|
601
|
+
|
|
602
|
+
steps_per_epoch = math.ceil(len(train_ex) / self.batch_size)
|
|
603
|
+
interval = self.validation_interval
|
|
604
|
+
if interval == "auto":
|
|
605
|
+
interval = math.ceil(steps_per_epoch / CHECKS_PER_EPOCH) if plan.learns else steps_per_epoch
|
|
606
|
+
|
|
607
|
+
fingerprint = self._fingerprint(examples)
|
|
608
|
+
saved = getattr(self, "training_state_", None)
|
|
609
|
+
resume = saved is not None and saved.fingerprint == fingerprint
|
|
610
|
+
if resume:
|
|
611
|
+
state = saved
|
|
612
|
+
self._log("resuming at epoch %g of %d (step %d)" % (state.step / steps_per_epoch, self.epochs, state.step))
|
|
613
|
+
else:
|
|
614
|
+
state = TrainState.fresh(fingerprint, self.seed)
|
|
615
|
+
if fresh:
|
|
616
|
+
self._log("training from scratch: up to %d epochs" % self.epochs)
|
|
617
|
+
elif saved is not None:
|
|
618
|
+
self._log("warm start (the training data or run settings differ from the saved run): "
|
|
619
|
+
"training %d epochs" % self.epochs)
|
|
620
|
+
else:
|
|
621
|
+
self._log("warm start (no saved training state): training %d epochs" % self.epochs)
|
|
622
|
+
|
|
623
|
+
net = Network(self.head_, self.device_, backbone=self.backbone_ if plan.learns else None,
|
|
624
|
+
arch=self.architecture_, pad_id=self.tokenizer_.pad_token_id, backbone_groups=backbone_groups,
|
|
625
|
+
autocast_dtype=self._autocast_dtype())
|
|
626
|
+
settings = TrainSettings(
|
|
627
|
+
epochs=self.epochs, batch_size=self.batch_size,
|
|
628
|
+
lrs=self._learning_rates(),
|
|
629
|
+
weight_decay=self.weight_decay, optimizer=self.optimizer, scheduler=self.scheduler,
|
|
630
|
+
warmup_ratio=self.warmup_ratio, grad_clip=self.grad_clip, early_stopping=self.early_stopping,
|
|
631
|
+
n_iter_no_change=self.n_iter_no_change, target_loss=self.target_loss, validation_interval=interval,
|
|
632
|
+
micro_batch=micro)
|
|
633
|
+
self.training_state_ = state
|
|
634
|
+
try:
|
|
635
|
+
train(net, train_items, len(train_ex), val_items, sample_weights(train_ex, self.balance), settings,
|
|
636
|
+
state, self.history_, self._log, resume=resume)
|
|
637
|
+
finally:
|
|
638
|
+
round_trained(backbone_groups, self.dtype_)
|
|
639
|
+
self.backbone_.eval()
|
|
640
|
+
self.report_ = {
|
|
641
|
+
"n_train": len(train_ex),
|
|
642
|
+
"n_validation": len(val_ex),
|
|
643
|
+
"skipped": dict(skipped),
|
|
644
|
+
"validation": evaluate_items(net, val_items) if val_items else None,
|
|
645
|
+
}
|
|
646
|
+
return self
|
|
647
|
+
|
|
648
|
+
# -- predict -------------------------------------------------------------------------------
|
|
649
|
+
|
|
650
|
+
def _check_fitted(self) -> None:
|
|
651
|
+
if not self.is_fitted:
|
|
652
|
+
raise NotFittedError("%s is not fitted: call fit() or load a saved model with %s.load() before "
|
|
653
|
+
"predicting; an untrained decision head gives noise"
|
|
654
|
+
% (type(self).__name__, type(self).__name__))
|
|
655
|
+
|
|
656
|
+
def predict(self, X, questions: Mapping | None = None):
|
|
657
|
+
"""Answer typed questions in the Jev `/v1/systemone` shape.
|
|
658
|
+
|
|
659
|
+
- `predict(state, questions)` -> one result dict;
|
|
660
|
+
- `predict([state, ...], questions)` -> a list of results, one per state;
|
|
661
|
+
- `predict(rows)` -> a list of results, where each row is `{"state": ..., "questions": {...}}`.
|
|
662
|
+
|
|
663
|
+
A state that is itself a list (conversation turns) must be wrapped: `predict([turns], questions)`.
|
|
664
|
+
"""
|
|
665
|
+
self._check_fitted()
|
|
666
|
+
single = False
|
|
667
|
+
if questions is not None:
|
|
668
|
+
if isinstance(X, (str, Mapping)):
|
|
669
|
+
X, single = [X], True
|
|
670
|
+
rows = [{"state": state, "questions": questions} for state in X]
|
|
671
|
+
else:
|
|
672
|
+
if isinstance(X, Mapping):
|
|
673
|
+
X, single = [X], True
|
|
674
|
+
rows = list(X)
|
|
675
|
+
examples, _ = examples_from(rows, None, labeled=False)
|
|
676
|
+
results = [{"answers": {}, "usage": {"input_tokens": 0, "output_tokens": 0}} for _ in rows]
|
|
677
|
+
probs, lengths = self._predict_examples(examples)
|
|
678
|
+
for ex, n_tokens, p in zip(examples, lengths, probs):
|
|
679
|
+
result = results[ex.row]
|
|
680
|
+
result["answers"][ex.qid] = answer(ex.question, p[: ex.question.k])
|
|
681
|
+
result["usage"]["input_tokens"] += n_tokens
|
|
682
|
+
return results[0] if single else results
|
|
683
|
+
|
|
684
|
+
def evaluate(self, data, questions: Mapping | None = None) -> dict:
|
|
685
|
+
"""Score the model on labelled data, in any form `fit` accepts.
|
|
686
|
+
|
|
687
|
+
Returns a plain dict with `overall`, `by_type`, `by_category` and `by_question` metrics
|
|
688
|
+
(accuracy, distance, log_loss, and score_error / auc where they apply). Hard and soft
|
|
689
|
+
labels are scored the same way; see `sysone.evaluation` for the definitions.
|
|
690
|
+
"""
|
|
691
|
+
self._check_fitted()
|
|
692
|
+
examples, skipped = examples_from(data, questions, labeled=True)
|
|
693
|
+
probs, _ = self._predict_examples(examples)
|
|
694
|
+
return score(examples, probs, skipped)
|
|
695
|
+
|
|
696
|
+
def _predict_examples(self, examples: list[Example]) -> tuple[list[list[float]], list[int]]:
|
|
697
|
+
"""Option probabilities (temperature-scaled) and sequence lengths for each example."""
|
|
698
|
+
if not examples:
|
|
699
|
+
return [], []
|
|
700
|
+
encoder = self._encoder()
|
|
701
|
+
encoded = [encoder.encode(ex.state, ex.question) for ex in examples]
|
|
702
|
+
feats = extract(self.architecture_, self.backbone_, encoded, self.tokenizer_.pad_token_id, self.device_,
|
|
703
|
+
self._autocast_dtype())
|
|
704
|
+
probs = self._probabilities(feats, [ex.question.type_index for ex in examples])
|
|
705
|
+
return probs, [len(e.input_ids) for e in encoded]
|
|
706
|
+
|
|
707
|
+
@torch.no_grad()
|
|
708
|
+
def _probabilities(self, feats, qtypes, batch_size: int = 256) -> list[list[float]]:
|
|
709
|
+
self.head_.eval()
|
|
710
|
+
temps = torch.tensor([self.temperatures_[t] for t in QUESTION_TYPES], device=self.device_)
|
|
711
|
+
out = []
|
|
712
|
+
for start in range(0, len(feats), batch_size):
|
|
713
|
+
options, mask, summary = collate_features(feats[start: start + batch_size], self.device_)
|
|
714
|
+
qtype = torch.tensor(qtypes[start: start + batch_size], dtype=torch.long, device=self.device_)
|
|
715
|
+
logits = self.head_(options, mask, summary, qtype) / temps[qtype][:, None]
|
|
716
|
+
out.extend(torch.softmax(logits, dim=-1).cpu().tolist())
|
|
717
|
+
return out
|
|
718
|
+
|
|
719
|
+
# -- save / load ---------------------------------------------------------------------------
|
|
720
|
+
|
|
721
|
+
def save(self, path, training_state="auto") -> Path:
|
|
722
|
+
"""Write a self-contained folder. Frozen backbone weights are not saved; `load` downloads
|
|
723
|
+
the base model by name and revision.
|
|
724
|
+
|
|
725
|
+
`training_state` decides whether what a later `fit` needs to resume this run exactly
|
|
726
|
+
(optimizer state, schedule position, last weights, random states) is included: "auto"
|
|
727
|
+
includes it when it is under 100 MB, True always, False never.
|
|
728
|
+
"""
|
|
729
|
+
from safetensors.torch import save_file
|
|
730
|
+
|
|
731
|
+
self._check_fitted()
|
|
732
|
+
if training_state not in ("auto", True, False):
|
|
733
|
+
raise ValueError("training_state must be 'auto', True or False, got %r" % (training_state,))
|
|
734
|
+
path = Path(path)
|
|
735
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
736
|
+
params = self.get_params()
|
|
737
|
+
params["architecture"] = None
|
|
738
|
+
config = {
|
|
739
|
+
"format": "sysone",
|
|
740
|
+
"sysone_version": __version__,
|
|
741
|
+
"class": type(self).__name__,
|
|
742
|
+
"params": params,
|
|
743
|
+
"architecture": architecture_spec(self.architecture_),
|
|
744
|
+
"base_model": {"name": self.model_name, "revision": self.base_revision_},
|
|
745
|
+
"prompt_format": self.prompt_format_.to_dict(),
|
|
746
|
+
"type_names": list(QUESTION_TYPES),
|
|
747
|
+
"temperatures": self.temperatures_,
|
|
748
|
+
"fitted": {
|
|
749
|
+
"max_state_tokens": self.max_state_tokens_,
|
|
750
|
+
"max_option_tokens": self.max_option_tokens_,
|
|
751
|
+
"max_len": self.max_len_,
|
|
752
|
+
"head_dim": self.head_dim_,
|
|
753
|
+
"num_layers": self.backbone_.config.num_hidden_layers,
|
|
754
|
+
"hidden_size": self.backbone_.config.hidden_size,
|
|
755
|
+
"tuning": self.tuning_,
|
|
756
|
+
"trainable_layers": self.plan_.n_full,
|
|
757
|
+
"lora_layers": self.plan_.n_lora,
|
|
758
|
+
"train_embeddings": self.plan_.train_embeddings,
|
|
759
|
+
},
|
|
760
|
+
"history": self.history_,
|
|
761
|
+
"report": getattr(self, "report_", None),
|
|
762
|
+
}
|
|
763
|
+
with open(path / CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
764
|
+
json.dump(config, f, indent=2, default=_json_default)
|
|
765
|
+
save_file({k: v.detach().cpu().contiguous() for k, v in self.head_.state_dict().items()},
|
|
766
|
+
str(path / HEAD_FILE))
|
|
767
|
+
for stale in (ADAPTER_DIR, BACKBONE_FILE, STATE_DIR): # from an earlier save to the same folder
|
|
768
|
+
target = path / stale
|
|
769
|
+
if target.is_dir():
|
|
770
|
+
shutil.rmtree(target)
|
|
771
|
+
elif target.exists():
|
|
772
|
+
target.unlink()
|
|
773
|
+
if self.plan_.n_lora:
|
|
774
|
+
save_adapter(self.backbone_, path / ADAPTER_DIR)
|
|
775
|
+
full_weights = trained_parameters(self.architecture_, self.backbone_, self.plan_)["full"]
|
|
776
|
+
if full_weights:
|
|
777
|
+
save_backbone_weights(full_weights, self.dtype_, path / BACKBONE_FILE)
|
|
778
|
+
state = getattr(self, "training_state_", None)
|
|
779
|
+
if state is not None and training_state is not False:
|
|
780
|
+
size = state.nbytes()
|
|
781
|
+
if training_state is True or size <= TRAINING_STATE_AUTO_BYTES:
|
|
782
|
+
save_training_state(state, path / STATE_DIR)
|
|
783
|
+
else:
|
|
784
|
+
self._log("training state (%s) not saved: over 100 MB; use save(path, training_state=True) to "
|
|
785
|
+
"include it so a later fit() resumes this run" % format_bytes(size))
|
|
786
|
+
return path
|
|
787
|
+
|
|
788
|
+
@classmethod
|
|
789
|
+
def load(cls, path, **overrides) -> SystemOneModel:
|
|
790
|
+
"""Load a model saved with `save`, from a local folder or a Hugging Face Hub repo id.
|
|
791
|
+
`overrides` replace saved runtime parameters such as `device` or `verbose`."""
|
|
792
|
+
from safetensors.torch import load_file
|
|
793
|
+
|
|
794
|
+
path = Path(path)
|
|
795
|
+
if not path.is_dir():
|
|
796
|
+
from huggingface_hub import snapshot_download
|
|
797
|
+
|
|
798
|
+
path = Path(snapshot_download(str(path)))
|
|
799
|
+
with open(path / CONFIG_FILE, encoding="utf-8") as f:
|
|
800
|
+
config = json.load(f)
|
|
801
|
+
if config.get("format") != "sysone":
|
|
802
|
+
raise ValueError("%s is not a saved sysone model" % path)
|
|
803
|
+
klass = _model_class(config["class"])
|
|
804
|
+
if not issubclass(klass, cls):
|
|
805
|
+
raise TypeError("%s holds a %s, not a %s" % (path, klass.__name__, cls.__name__))
|
|
806
|
+
params = _upgrade_params(dict(config["params"]))
|
|
807
|
+
spec = config["architecture"]
|
|
808
|
+
params["architecture"] = None if spec in _registered_specs() else architecture_from_spec(spec)
|
|
809
|
+
params.update(overrides)
|
|
810
|
+
model = klass(**params)
|
|
811
|
+
|
|
812
|
+
fitted = config["fitted"]
|
|
813
|
+
tuning = fitted.get("tuning", "head")
|
|
814
|
+
if tuning == "qlora":
|
|
815
|
+
tuning = effective_tuning(tuning, resolve_device(model.device), stacklevel=2)
|
|
816
|
+
model._load_backbone(fitted["num_layers"], config["base_model"]["revision"], tuning)
|
|
817
|
+
if model.backbone_.config.hidden_size != fitted["hidden_size"]:
|
|
818
|
+
raise ValueError("the base model's hidden size changed since this model was saved")
|
|
819
|
+
model.plan_ = LayerPlan(fitted["num_layers"], fitted.get("trainable_layers", 0), fitted.get("lora_layers", 0),
|
|
820
|
+
fitted.get("train_embeddings", False))
|
|
821
|
+
if (path / ADAPTER_DIR).is_dir():
|
|
822
|
+
load_adapter(model.backbone_, path / ADAPTER_DIR)
|
|
823
|
+
if (path / BACKBONE_FILE).is_file():
|
|
824
|
+
load_backbone_weights(model.backbone_, path / BACKBONE_FILE)
|
|
825
|
+
model.backbone_.eval()
|
|
826
|
+
model.prompt_format_ = PromptFormat.from_dict(config["prompt_format"])
|
|
827
|
+
model.max_state_tokens_ = fitted["max_state_tokens"]
|
|
828
|
+
model.max_option_tokens_ = fitted["max_option_tokens"]
|
|
829
|
+
model.max_len_ = fitted["max_len"]
|
|
830
|
+
model.head_dim_ = fitted["head_dim"]
|
|
831
|
+
head = CompactHead(fitted["hidden_size"], fitted["head_dim"], model.head_layers, model.head_dropout)
|
|
832
|
+
state = load_file(str(path / HEAD_FILE))
|
|
833
|
+
state["type_embedding.weight"] = _reorder_types(state["type_embedding.weight"], config["type_names"])
|
|
834
|
+
head.load_state_dict(state)
|
|
835
|
+
model.head_ = head.to(model.device_).eval()
|
|
836
|
+
model.temperatures_ = {t: float(config["temperatures"].get(t, 1.0)) for t in QUESTION_TYPES}
|
|
837
|
+
model.history_ = config.get("history") or []
|
|
838
|
+
model.report_ = config.get("report")
|
|
839
|
+
model.training_state_ = load_training_state(path / STATE_DIR) if (path / STATE_DIR).is_dir() else None
|
|
840
|
+
model.structure_ = model._structure()
|
|
841
|
+
return model
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
def _upgrade_params(params: dict) -> dict:
|
|
845
|
+
"""Saved parameters from earlier versions, in this version's terms: `encoder_lr` and
|
|
846
|
+
`adapter_lr` became `backbone_lr`. A value changed from its old default is kept."""
|
|
847
|
+
encoder_lr, adapter_lr = params.pop("encoder_lr", 2.5e-5), params.pop("adapter_lr", 1e-4)
|
|
848
|
+
if "backbone_lr" not in params:
|
|
849
|
+
if params.get("tuning") in ("lora", "qlora") and adapter_lr != 1e-4:
|
|
850
|
+
params["backbone_lr"] = adapter_lr
|
|
851
|
+
elif params.get("tuning") == "full" and encoder_lr != 2.5e-5:
|
|
852
|
+
params["backbone_lr"] = encoder_lr
|
|
853
|
+
return params
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _json_default(value):
|
|
857
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
858
|
+
return None
|
|
859
|
+
if isinstance(value, (set, tuple)):
|
|
860
|
+
return list(value)
|
|
861
|
+
raise TypeError("cannot save %r in the config" % (value,))
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _all_subclasses(cls):
|
|
865
|
+
for sub in cls.__subclasses__():
|
|
866
|
+
yield sub
|
|
867
|
+
yield from _all_subclasses(sub)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _model_class(name: str) -> type[SystemOneModel]:
|
|
871
|
+
for klass in _all_subclasses(SystemOneModel):
|
|
872
|
+
if klass.__name__ == name:
|
|
873
|
+
return klass
|
|
874
|
+
raise ValueError("unknown model class %r" % name)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _registered_specs() -> set[str]:
|
|
878
|
+
from .architectures.registry import _REGISTRY
|
|
879
|
+
|
|
880
|
+
return set(_REGISTRY)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _reorder_types(weight: torch.Tensor, saved_names: list[str]) -> torch.Tensor:
|
|
884
|
+
"""Put type-embedding rows in this version's type order, matching them by name."""
|
|
885
|
+
if list(saved_names) == list(QUESTION_TYPES):
|
|
886
|
+
return weight
|
|
887
|
+
missing = set(QUESTION_TYPES) - set(saved_names)
|
|
888
|
+
if missing:
|
|
889
|
+
raise ValueError("the saved model has no embedding for question types %s" % sorted(missing))
|
|
890
|
+
return torch.stack([weight[saved_names.index(name)] for name in QUESTION_TYPES])
|