mostlyai-engine 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.
- mostlyai/engine/__init__.py +27 -0
- mostlyai/engine/_common.py +612 -0
- mostlyai/engine/_dtypes.py +58 -0
- mostlyai/engine/_encoding_types/__init__.py +13 -0
- mostlyai/engine/_encoding_types/language/__init__.py +13 -0
- mostlyai/engine/_encoding_types/language/text.py +41 -0
- mostlyai/engine/_encoding_types/tabular/__init__.py +13 -0
- mostlyai/engine/_encoding_types/tabular/categorical.py +115 -0
- mostlyai/engine/_encoding_types/tabular/character.py +155 -0
- mostlyai/engine/_encoding_types/tabular/datetime.py +254 -0
- mostlyai/engine/_encoding_types/tabular/itt.py +352 -0
- mostlyai/engine/_encoding_types/tabular/lat_long.py +353 -0
- mostlyai/engine/_encoding_types/tabular/numeric.py +548 -0
- mostlyai/engine/_language/__init__.py +17 -0
- mostlyai/engine/_language/common.py +110 -0
- mostlyai/engine/_language/encoding.py +162 -0
- mostlyai/engine/_language/engine/__init__.py +13 -0
- mostlyai/engine/_language/engine/base.py +51 -0
- mostlyai/engine/_language/engine/hf_engine.py +136 -0
- mostlyai/engine/_language/engine/vllm_engine.py +187 -0
- mostlyai/engine/_language/formatron_utils.py +114 -0
- mostlyai/engine/_language/generation.py +309 -0
- mostlyai/engine/_language/lstm.py +139 -0
- mostlyai/engine/_language/tokenizer_utils.py +144 -0
- mostlyai/engine/_language/training.py +718 -0
- mostlyai/engine/_memory.py +69 -0
- mostlyai/engine/_tabular/__init__.py +13 -0
- mostlyai/engine/_tabular/argn.py +1382 -0
- mostlyai/engine/_tabular/common.py +29 -0
- mostlyai/engine/_tabular/encoding.py +406 -0
- mostlyai/engine/_tabular/fairness.py +245 -0
- mostlyai/engine/_tabular/generation.py +1157 -0
- mostlyai/engine/_tabular/training.py +856 -0
- mostlyai/engine/_training_utils.py +159 -0
- mostlyai/engine/_workspace.py +225 -0
- mostlyai/engine/analysis.py +571 -0
- mostlyai/engine/domain.py +171 -0
- mostlyai/engine/encoding.py +42 -0
- mostlyai/engine/generation.py +110 -0
- mostlyai/engine/splitting.py +257 -0
- mostlyai/engine/training.py +104 -0
- mostlyai_engine-1.0.0.dist-info/METADATA +179 -0
- mostlyai_engine-1.0.0.dist-info/RECORD +45 -0
- mostlyai_engine-1.0.0.dist-info/WHEEL +4 -0
- mostlyai_engine-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
# Copyright 2025 MOSTLY AI
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import math
|
|
18
|
+
import time
|
|
19
|
+
from contextlib import nullcontext
|
|
20
|
+
from functools import partial
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
import warnings
|
|
24
|
+
|
|
25
|
+
from importlib.metadata import version
|
|
26
|
+
import pandas as pd
|
|
27
|
+
import numpy as np
|
|
28
|
+
import torch
|
|
29
|
+
from opacus.grad_sample import register_grad_sampler
|
|
30
|
+
from torch import nn
|
|
31
|
+
from torch.nn import CrossEntropyLoss
|
|
32
|
+
from torch.optim.lr_scheduler import LRScheduler
|
|
33
|
+
|
|
34
|
+
from opacus import PrivacyEngine, GradSampleModule
|
|
35
|
+
from opacus.accountants import PRVAccountant, RDPAccountant, GaussianAccountant
|
|
36
|
+
from opacus.utils.batch_memory_manager import wrap_data_loader
|
|
37
|
+
|
|
38
|
+
from torch.utils.data import DataLoader
|
|
39
|
+
|
|
40
|
+
from mostlyai.engine._common import (
|
|
41
|
+
STRING,
|
|
42
|
+
ProgressCallback,
|
|
43
|
+
ProgressCallbackWrapper,
|
|
44
|
+
TABLE_COLUMN_INFIX,
|
|
45
|
+
)
|
|
46
|
+
from mostlyai.engine._language.common import (
|
|
47
|
+
is_bf16_supported,
|
|
48
|
+
load_base_model_and_config,
|
|
49
|
+
estimate_max_tokens,
|
|
50
|
+
MAX_LENGTH,
|
|
51
|
+
)
|
|
52
|
+
from mostlyai.engine._language.encoding import row_to_json
|
|
53
|
+
from mostlyai.engine._language.tokenizer_utils import (
|
|
54
|
+
train_tokenizer,
|
|
55
|
+
MostlyDataCollatorForLanguageModeling,
|
|
56
|
+
tokenize_fn,
|
|
57
|
+
)
|
|
58
|
+
from mostlyai.engine._training_utils import (
|
|
59
|
+
check_early_training_exit,
|
|
60
|
+
EarlyStopper,
|
|
61
|
+
ModelCheckpoint,
|
|
62
|
+
ProgressMessage,
|
|
63
|
+
)
|
|
64
|
+
from mostlyai.engine.domain import ModelStateStrategy, DifferentialPrivacyConfig
|
|
65
|
+
from mostlyai.engine._workspace import Workspace, ensure_workspace_dir
|
|
66
|
+
from datasets import load_dataset, DatasetDict, Dataset, disable_progress_bar
|
|
67
|
+
from transformers import (
|
|
68
|
+
AutoTokenizer,
|
|
69
|
+
PreTrainedModel,
|
|
70
|
+
)
|
|
71
|
+
from mostlyai.engine._language.lstm import LSTMFromScratchLMHeadModel, LSTMFromScratchConfig
|
|
72
|
+
from peft import LoraConfig, PeftModel
|
|
73
|
+
|
|
74
|
+
_LOG = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
#####################
|
|
78
|
+
### TRAINING LOOP ###
|
|
79
|
+
#####################
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _training_batch_size_heuristic(no_of_records: int, no_of_model_params: int, max_tokens: int) -> tuple[int, int]:
|
|
83
|
+
"""
|
|
84
|
+
Calculate the physical batch size and gradient accumulation steps.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
no_of_records (int): Number of records in the training dataset.
|
|
88
|
+
no_of_model_params (int): Number of model parameters.
|
|
89
|
+
max_tokens (int): Maximum number of tokens that are in the training dataset.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Tuple[int, int]: A tuple containing:
|
|
93
|
+
- Batch size (int)
|
|
94
|
+
- Gradient accumulation steps (int)
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
if no_of_model_params < 10_000_000:
|
|
98
|
+
batch_size = 32
|
|
99
|
+
elif no_of_model_params < 2_000_000_000:
|
|
100
|
+
batch_size = 16 if max_tokens < 100 else 8
|
|
101
|
+
else:
|
|
102
|
+
batch_size = 8 if max_tokens < 100 else 4
|
|
103
|
+
gradient_accumulation_steps = 2
|
|
104
|
+
max_batch_size = no_of_records // gradient_accumulation_steps
|
|
105
|
+
batch_size = int(np.clip(a=batch_size, a_min=1, a_max=max_batch_size))
|
|
106
|
+
return batch_size, gradient_accumulation_steps
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _learn_rate_heuristic(no_of_model_params: int) -> float:
|
|
110
|
+
if no_of_model_params < 10_000_000:
|
|
111
|
+
learn_rate = 4e-4
|
|
112
|
+
else:
|
|
113
|
+
learn_rate = 2e-5
|
|
114
|
+
return learn_rate
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@register_grad_sampler(nn.Linear)
|
|
118
|
+
def compute_linear_grad_sample_full_precision(
|
|
119
|
+
layer: nn.Linear, activations: list[torch.Tensor], backprops: torch.Tensor
|
|
120
|
+
) -> dict[nn.Parameter, torch.Tensor]:
|
|
121
|
+
"""
|
|
122
|
+
Overwrite the default backward hook for linear layer implemented in
|
|
123
|
+
https://github.com/pytorch/opacus/blob/main/opacus/grad_sample/linear.py#L29-L48
|
|
124
|
+
|
|
125
|
+
The difference is that this ensures activations and backprops are upcasted to float32 before the computation.
|
|
126
|
+
"""
|
|
127
|
+
activations = activations[0]
|
|
128
|
+
ret = {}
|
|
129
|
+
if layer.weight.requires_grad:
|
|
130
|
+
gs = torch.einsum("n...i,n...j->nij", backprops.float(), activations.float())
|
|
131
|
+
ret[layer.weight] = gs
|
|
132
|
+
if layer.bias is not None and layer.bias.requires_grad:
|
|
133
|
+
ret[layer.bias] = torch.einsum("n...k->nk", backprops.float())
|
|
134
|
+
return ret
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class LanguageModelCheckpoint(ModelCheckpoint):
|
|
138
|
+
def _save_model_weights(self, model: PreTrainedModel | GradSampleModule) -> None:
|
|
139
|
+
if isinstance(model, GradSampleModule):
|
|
140
|
+
# LSTMFromScratchLMHeadModel with DPLSTM layers can only be saved without safe serialization
|
|
141
|
+
# the weights will be saved as *.bin instead of .safetensors
|
|
142
|
+
safe_serialization = model._module.config.model_type != LSTMFromScratchConfig.model_type
|
|
143
|
+
model._module.save_pretrained(self.workspace.model_path, safe_serialization=safe_serialization)
|
|
144
|
+
else:
|
|
145
|
+
model.save_pretrained(self.workspace.model_path)
|
|
146
|
+
|
|
147
|
+
def _clear_model_weights(self) -> None:
|
|
148
|
+
patterns = ["*.safetensors", "*.bin", "*.json"]
|
|
149
|
+
files = [f for p in patterns for f in self.workspace.model_path.glob(p)]
|
|
150
|
+
for f in files:
|
|
151
|
+
f.unlink(missing_ok=True)
|
|
152
|
+
|
|
153
|
+
def model_weights_path_exists(self) -> bool:
|
|
154
|
+
return any(self.workspace.model_path.glob("*.safetensors")) or any(self.workspace.model_path.glob("*.bin"))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _calculate_per_label_losses(
|
|
158
|
+
model: PreTrainedModel | GradSampleModule, step_data: dict
|
|
159
|
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
160
|
+
outputs = model(input_ids=step_data["input_ids"], attention_mask=step_data["attention_mask"])
|
|
161
|
+
logits = outputs.logits
|
|
162
|
+
|
|
163
|
+
labels = step_data["labels"]
|
|
164
|
+
# Shift so that tokens < n predict n
|
|
165
|
+
shift_logits = logits[..., :-1, :].contiguous()
|
|
166
|
+
shift_labels = labels[..., 1:].contiguous()
|
|
167
|
+
# Flatten the tokens
|
|
168
|
+
shift_logits = shift_logits.view(
|
|
169
|
+
-1, model.config.vocab_size if not isinstance(model, GradSampleModule) else model._module.config.vocab_size
|
|
170
|
+
)
|
|
171
|
+
shift_labels = shift_labels.view(-1)
|
|
172
|
+
# Ensure tensors are on the same device
|
|
173
|
+
shift_labels = shift_labels.to(shift_logits.device)
|
|
174
|
+
loss_fct = CrossEntropyLoss(reduction="sum")
|
|
175
|
+
loss = loss_fct(shift_logits, shift_labels)
|
|
176
|
+
labels_ignored = torch.sum(shift_labels == -100)
|
|
177
|
+
num_labels = shift_labels.numel() - labels_ignored
|
|
178
|
+
return loss, num_labels
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@torch.no_grad()
|
|
182
|
+
def _calculate_val_loss(model: PreTrainedModel | GradSampleModule, val_dataloader: DataLoader) -> float:
|
|
183
|
+
device = model.device if not isinstance(model, GradSampleModule) else model._module.device
|
|
184
|
+
total_loss = torch.tensor(0, dtype=torch.float32, device=device)
|
|
185
|
+
total_num_labels = torch.tensor(0, dtype=torch.long, device=device)
|
|
186
|
+
model.eval()
|
|
187
|
+
for step_data in val_dataloader:
|
|
188
|
+
step_data = {k: v.to(device) for k, v in step_data.items()}
|
|
189
|
+
loss, num_labels = _calculate_per_label_losses(model, step_data)
|
|
190
|
+
total_loss += loss
|
|
191
|
+
total_num_labels += num_labels
|
|
192
|
+
model.train()
|
|
193
|
+
val_loss_avg = total_loss / total_num_labels
|
|
194
|
+
return val_loss_avg.item()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def train(
|
|
198
|
+
*,
|
|
199
|
+
model: str = "MOSTLY_AI/LSTMFromScratch-3m",
|
|
200
|
+
max_training_time: float = 60.0,
|
|
201
|
+
max_epochs: float = 100.0,
|
|
202
|
+
batch_size: int | None = None,
|
|
203
|
+
gradient_accumulation_steps: int | None = None,
|
|
204
|
+
enable_flexible_generation: bool = True,
|
|
205
|
+
differential_privacy: DifferentialPrivacyConfig | dict | None = None,
|
|
206
|
+
upload_model_data_callback: Callable | None = None,
|
|
207
|
+
model_state_strategy: ModelStateStrategy | str = ModelStateStrategy.reset,
|
|
208
|
+
device: torch.device | str | None = None,
|
|
209
|
+
workspace_dir: str | Path = "engine-ws",
|
|
210
|
+
update_progress: ProgressCallback | None = None,
|
|
211
|
+
):
|
|
212
|
+
_LOG.info("TRAIN_LANGUAGE started")
|
|
213
|
+
t0_ = time.time()
|
|
214
|
+
workspace_dir = ensure_workspace_dir(workspace_dir)
|
|
215
|
+
workspace = Workspace(workspace_dir)
|
|
216
|
+
|
|
217
|
+
with ProgressCallbackWrapper(
|
|
218
|
+
update_progress, progress_messages_path=workspace.model_progress_messages_path
|
|
219
|
+
) as progress:
|
|
220
|
+
_LOG.info(f"numpy={version('numpy')}, pandas={version('pandas')}")
|
|
221
|
+
_LOG.info(f"torch={version('torch')}, opacus={version('opacus')}")
|
|
222
|
+
_LOG.info(f"transformers={version('transformers')}, peft={version('peft')}")
|
|
223
|
+
device = (
|
|
224
|
+
torch.device(device)
|
|
225
|
+
if device is not None
|
|
226
|
+
else (torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu"))
|
|
227
|
+
)
|
|
228
|
+
_LOG.info(f"{device=}")
|
|
229
|
+
bf16_supported = is_bf16_supported(device)
|
|
230
|
+
_LOG.info(f"{bf16_supported=}")
|
|
231
|
+
use_mixed_precision = bf16_supported and model != LSTMFromScratchConfig.model_id
|
|
232
|
+
_LOG.info(f"{use_mixed_precision=}")
|
|
233
|
+
|
|
234
|
+
tgt_stats = workspace.tgt_stats.read()
|
|
235
|
+
trn_cnt = tgt_stats["no_of_training_records"]
|
|
236
|
+
val_cnt = tgt_stats["no_of_validation_records"]
|
|
237
|
+
|
|
238
|
+
# set defaults
|
|
239
|
+
model_id = model or LSTMFromScratchConfig.model_id
|
|
240
|
+
_LOG.info(f"{model_id=}")
|
|
241
|
+
_LOG.info(f"{enable_flexible_generation=}")
|
|
242
|
+
max_training_time = max(0.0, max_training_time * 60) # convert to seconds
|
|
243
|
+
_LOG.info(f"{max_training_time=}s")
|
|
244
|
+
max_epochs = max(0.0, max_epochs)
|
|
245
|
+
max_epochs_cap = math.ceil((trn_cnt + val_cnt) / 50)
|
|
246
|
+
if max_epochs_cap < max_epochs:
|
|
247
|
+
_LOG.info(f"{max_epochs=} -> max_epochs={max_epochs_cap} due to small sample size")
|
|
248
|
+
max_epochs = max_epochs_cap
|
|
249
|
+
else:
|
|
250
|
+
_LOG.info(f"{max_epochs=}")
|
|
251
|
+
with_dp = differential_privacy is not None
|
|
252
|
+
_LOG.info(f"{with_dp=}")
|
|
253
|
+
_LOG.info(f"{model_state_strategy=}")
|
|
254
|
+
|
|
255
|
+
# initialize callbacks
|
|
256
|
+
upload_model_data_callback = upload_model_data_callback or (lambda *args, **kwargs: None)
|
|
257
|
+
|
|
258
|
+
# the line below fixes issue with growing epoch time for later epochs
|
|
259
|
+
# https://discuss.pytorch.org/t/training-time-gets-slower-and-slower-on-cpu/145483
|
|
260
|
+
torch.set_flush_denormal(True)
|
|
261
|
+
|
|
262
|
+
# load raw encoded data
|
|
263
|
+
if check_early_training_exit(workspace=workspace, trn_cnt=trn_cnt, val_cnt=val_cnt):
|
|
264
|
+
empty_ds = Dataset.from_dict({"ctx": [], "tgt": []})
|
|
265
|
+
raw_dataset = DatasetDict({"train": empty_ds, "validation": empty_ds})
|
|
266
|
+
else:
|
|
267
|
+
data_files = {
|
|
268
|
+
"train": [str(f) for f in workspace.encoded_data_trn.fetch_all()],
|
|
269
|
+
"validation": [str(f) for f in workspace.encoded_data_val.fetch_all()],
|
|
270
|
+
}
|
|
271
|
+
disable_progress_bar()
|
|
272
|
+
raw_dataset = load_dataset("parquet", data_files=data_files)
|
|
273
|
+
|
|
274
|
+
def shuffle_tgt_columns(x):
|
|
275
|
+
x_tgt = pd.DataFrame([json.loads(x.pop("tgt"))], dtype=STRING) # convert to DataFrame
|
|
276
|
+
x_tgt = x_tgt.sample(frac=1, axis=1) # shuffle columns
|
|
277
|
+
x_tgt = row_to_json(
|
|
278
|
+
x_tgt.add_prefix("tgt" + TABLE_COLUMN_INFIX).squeeze(axis=0), is_target=True
|
|
279
|
+
) # convert back to JSON
|
|
280
|
+
return x | {"tgt": x_tgt}
|
|
281
|
+
|
|
282
|
+
# shuffle target columns if flexible generation is enabled
|
|
283
|
+
anyorder_dataset = raw_dataset.map(shuffle_tgt_columns) if enable_flexible_generation else raw_dataset
|
|
284
|
+
|
|
285
|
+
def concat_prompt_and_response(x):
|
|
286
|
+
return {"content": "".join(x.values())}
|
|
287
|
+
|
|
288
|
+
# concatenate prompt and response to form the content
|
|
289
|
+
content_dataset = anyorder_dataset.map(
|
|
290
|
+
concat_prompt_and_response, remove_columns=anyorder_dataset["train"].column_names
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
tokenizer_args = {
|
|
294
|
+
"padding_side": "right",
|
|
295
|
+
"truncation_side": "right",
|
|
296
|
+
# these must be False at initialization, as we manually add them later in tokenize_fn
|
|
297
|
+
"add_bos_token": False,
|
|
298
|
+
"add_eos_token": False,
|
|
299
|
+
"legacy": True,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_LOG.info("create training model")
|
|
303
|
+
model_checkpoint = LanguageModelCheckpoint(workspace=workspace)
|
|
304
|
+
model: PreTrainedModel | PeftModel
|
|
305
|
+
|
|
306
|
+
# check how to handle existing model weights
|
|
307
|
+
if isinstance(model_state_strategy, str):
|
|
308
|
+
model_state_strategy = ModelStateStrategy(model_state_strategy)
|
|
309
|
+
if not model_checkpoint.model_weights_path_exists():
|
|
310
|
+
_LOG.info(f"model weights not found; change strategy from {model_state_strategy} to RESET")
|
|
311
|
+
model_state_strategy = ModelStateStrategy.reset
|
|
312
|
+
_LOG.info(f"{model_state_strategy=}")
|
|
313
|
+
if model_state_strategy in [ModelStateStrategy.resume, ModelStateStrategy.reuse]:
|
|
314
|
+
_LOG.info("load existing model weights")
|
|
315
|
+
torch.serialization.add_safe_globals([np.core.multiarray.scalar, np.dtype, np.dtypes.Float64DType])
|
|
316
|
+
resume_from_last_checkpoint = True
|
|
317
|
+
model_id_or_path = workspace.model_path
|
|
318
|
+
else: # ModelStateStrategy.reset
|
|
319
|
+
_LOG.info("clear existing checkpoint files")
|
|
320
|
+
model_checkpoint.clear_checkpoint()
|
|
321
|
+
resume_from_last_checkpoint = False
|
|
322
|
+
model_id_or_path = model
|
|
323
|
+
|
|
324
|
+
# check how to handle existing progress state
|
|
325
|
+
last_progress_message = progress.get_last_progress_message()
|
|
326
|
+
if last_progress_message and model_state_strategy == ModelStateStrategy.resume:
|
|
327
|
+
epoch = last_progress_message.get("epoch", 0.0)
|
|
328
|
+
steps = last_progress_message.get("steps", 0)
|
|
329
|
+
samples = last_progress_message.get("samples", 0)
|
|
330
|
+
initial_lr = last_progress_message.get("learn_rate", None)
|
|
331
|
+
total_time_init = last_progress_message.get("total_time", 0.0)
|
|
332
|
+
else:
|
|
333
|
+
epoch = 0.0
|
|
334
|
+
steps = 0
|
|
335
|
+
samples = 0
|
|
336
|
+
initial_lr = None
|
|
337
|
+
total_time_init = 0.0
|
|
338
|
+
progress.reset_progress_messages()
|
|
339
|
+
_LOG.info(f"start training progress from {epoch=}, {steps=}")
|
|
340
|
+
|
|
341
|
+
t0 = time.time()
|
|
342
|
+
if model == LSTMFromScratchConfig.model_id:
|
|
343
|
+
if resume_from_last_checkpoint:
|
|
344
|
+
tokenizer = AutoTokenizer.from_pretrained(model_id_or_path, **tokenizer_args)
|
|
345
|
+
model, _ = load_base_model_and_config(
|
|
346
|
+
model_id_or_path, device=device, is_peft_adapter=False, is_training=True
|
|
347
|
+
)
|
|
348
|
+
else:
|
|
349
|
+
# fresh initialization of the custom tokenizer and LSTM model
|
|
350
|
+
tokenizer_train_iter = (
|
|
351
|
+
content_dataset["train"][i : i + 1_000]["content"]
|
|
352
|
+
for i in range(0, len(content_dataset["train"]), 1_000)
|
|
353
|
+
)
|
|
354
|
+
# train a custom tokenizer and convert it to a LlamaTokenizerFast object
|
|
355
|
+
tokenizer = train_tokenizer(tokenizer_train_iter, tokenizer_kwargs=tokenizer_args)
|
|
356
|
+
model_config = LSTMFromScratchConfig(vocab_size=len(tokenizer), with_dp=with_dp)
|
|
357
|
+
model = LSTMFromScratchLMHeadModel(model_config).to(device)
|
|
358
|
+
else:
|
|
359
|
+
model, model_config = load_base_model_and_config(
|
|
360
|
+
model_id_or_path,
|
|
361
|
+
device=device,
|
|
362
|
+
is_peft_adapter=resume_from_last_checkpoint,
|
|
363
|
+
is_training=True,
|
|
364
|
+
)
|
|
365
|
+
tokenizer = AutoTokenizer.from_pretrained(model_id_or_path, **tokenizer_args)
|
|
366
|
+
if tokenizer.eos_token is None:
|
|
367
|
+
if getattr(model_config, "eos_token_id", None) is not None:
|
|
368
|
+
tokenizer.eos_token_id = model_config.eos_token_id
|
|
369
|
+
if tokenizer.bos_token is None:
|
|
370
|
+
if getattr(model_config, "bos_token_id", None) is not None:
|
|
371
|
+
tokenizer.bos_token_id = model_config.bos_token_id
|
|
372
|
+
else:
|
|
373
|
+
tokenizer.bos_token = tokenizer.eos_token
|
|
374
|
+
_LOG.warning("bos token not found, setting eos token as bos token")
|
|
375
|
+
if getattr(tokenizer, "pad_token", None) is None:
|
|
376
|
+
if getattr(tokenizer, "unk_token", None) is not None:
|
|
377
|
+
# warning: unk token can be valid output, although very unlikely for proper tokenizers
|
|
378
|
+
tokenizer.pad_token = tokenizer.unk_token
|
|
379
|
+
else:
|
|
380
|
+
_LOG.warning(
|
|
381
|
+
"pad_token not found and unk token not available as fallback, setting eos token as pad token -- this can result in eos being masked."
|
|
382
|
+
)
|
|
383
|
+
tokenizer.pad_token = tokenizer.eos_token
|
|
384
|
+
if resume_from_last_checkpoint:
|
|
385
|
+
model = PeftModel.from_pretrained(model, model_id_or_path, is_trainable=True)
|
|
386
|
+
else:
|
|
387
|
+
peft_config = LoraConfig(
|
|
388
|
+
lora_alpha=32, # 2x rank
|
|
389
|
+
lora_dropout=0.05,
|
|
390
|
+
r=16,
|
|
391
|
+
target_modules="all-linear",
|
|
392
|
+
bias="none",
|
|
393
|
+
task_type="CAUSAL_LM",
|
|
394
|
+
)
|
|
395
|
+
model.add_adapter(peft_config)
|
|
396
|
+
|
|
397
|
+
_LOG.info(f"model loading time: {time.time() - t0:.2f}s")
|
|
398
|
+
model.train()
|
|
399
|
+
no_of_model_params = model.num_parameters()
|
|
400
|
+
_LOG.info(f"{no_of_model_params=}")
|
|
401
|
+
no_of_trainable_model_params = model.num_parameters(only_trainable=True)
|
|
402
|
+
_LOG.info(f"{no_of_trainable_model_params=}")
|
|
403
|
+
|
|
404
|
+
_LOG.info(f"{tokenizer=}")
|
|
405
|
+
tokenizer.save_pretrained(workspace.model_path)
|
|
406
|
+
|
|
407
|
+
tokenized_datasets = content_dataset.map(
|
|
408
|
+
partial(
|
|
409
|
+
tokenize_fn,
|
|
410
|
+
tokenizer=tokenizer,
|
|
411
|
+
text_key="content",
|
|
412
|
+
add_bos_token=True,
|
|
413
|
+
add_eos_token=True,
|
|
414
|
+
padding=True,
|
|
415
|
+
truncation=True,
|
|
416
|
+
max_length=MAX_LENGTH,
|
|
417
|
+
),
|
|
418
|
+
batched=True,
|
|
419
|
+
remove_columns=content_dataset["train"].column_names,
|
|
420
|
+
)
|
|
421
|
+
data_collator = MostlyDataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
|
|
422
|
+
|
|
423
|
+
max_tokens_estimate = estimate_max_tokens(tgt_stats)
|
|
424
|
+
default_batch_size, default_gradient_accumulation_steps = _training_batch_size_heuristic(
|
|
425
|
+
no_of_records=trn_cnt, no_of_model_params=no_of_model_params, max_tokens=max_tokens_estimate
|
|
426
|
+
)
|
|
427
|
+
if batch_size is None:
|
|
428
|
+
batch_size = default_batch_size
|
|
429
|
+
if gradient_accumulation_steps is None:
|
|
430
|
+
gradient_accumulation_steps = default_gradient_accumulation_steps
|
|
431
|
+
|
|
432
|
+
# setup params for input pipeline
|
|
433
|
+
batch_size = max(1, min(batch_size, trn_cnt))
|
|
434
|
+
gradient_accumulation_steps = max(1, min(gradient_accumulation_steps, trn_cnt // batch_size))
|
|
435
|
+
trn_batch_size = batch_size * gradient_accumulation_steps
|
|
436
|
+
trn_steps = max(1, trn_cnt // trn_batch_size)
|
|
437
|
+
val_batch_size = max(1, min(batch_size, val_cnt))
|
|
438
|
+
val_steps = max(1, val_cnt // val_batch_size)
|
|
439
|
+
|
|
440
|
+
if initial_lr is None:
|
|
441
|
+
initial_lr = _learn_rate_heuristic(no_of_model_params)
|
|
442
|
+
|
|
443
|
+
_LOG.info(f"{trn_cnt=}, {val_cnt=}")
|
|
444
|
+
_LOG.info(f"{trn_batch_size=}, {val_batch_size=}")
|
|
445
|
+
_LOG.info(f"{trn_steps=}, {val_steps=}")
|
|
446
|
+
_LOG.info(f"{batch_size=}, {gradient_accumulation_steps=}, {initial_lr=}")
|
|
447
|
+
|
|
448
|
+
# early exit if there is not enough data to train the model
|
|
449
|
+
if len(tokenized_datasets["train"]) == 0 or len(tokenized_datasets["validation"]) == 0:
|
|
450
|
+
_LOG.warning("not enough data to train model; skipping training")
|
|
451
|
+
model.save_pretrained(workspace.model_path)
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
trn_dataloader = DataLoader(
|
|
455
|
+
tokenized_datasets["train"],
|
|
456
|
+
shuffle=True,
|
|
457
|
+
# either DP logical batch size or grad accumulation physical batch size
|
|
458
|
+
batch_size=trn_batch_size if with_dp else batch_size,
|
|
459
|
+
collate_fn=data_collator,
|
|
460
|
+
)
|
|
461
|
+
val_dataloader = DataLoader(
|
|
462
|
+
tokenized_datasets["validation"],
|
|
463
|
+
shuffle=False,
|
|
464
|
+
batch_size=val_batch_size,
|
|
465
|
+
collate_fn=data_collator,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
early_stopper = EarlyStopper(val_loss_patience=4)
|
|
469
|
+
optimizer = torch.optim.AdamW(params=model.parameters(), lr=initial_lr)
|
|
470
|
+
lr_scheduler: LRScheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
471
|
+
optimizer=optimizer,
|
|
472
|
+
factor=0.5,
|
|
473
|
+
patience=2,
|
|
474
|
+
min_lr=0.1 * initial_lr,
|
|
475
|
+
# threshold=0, # if we prefer to completely mimic the behavior of previous implementation
|
|
476
|
+
)
|
|
477
|
+
if (
|
|
478
|
+
model_state_strategy == ModelStateStrategy.resume
|
|
479
|
+
and model_checkpoint.optimizer_and_lr_scheduler_paths_exist()
|
|
480
|
+
):
|
|
481
|
+
# restore the full states of optimizer and lr_scheduler when possible
|
|
482
|
+
# otherwise, only the learning rate from the last progress message will be restored
|
|
483
|
+
_LOG.info("restore optimizer and LR scheduler states")
|
|
484
|
+
optimizer.load_state_dict(
|
|
485
|
+
torch.load(workspace.model_optimizer_path, map_location=device, weights_only=True)
|
|
486
|
+
)
|
|
487
|
+
lr_scheduler.load_state_dict(
|
|
488
|
+
torch.load(workspace.model_lr_scheduler_path, map_location=device, weights_only=True)
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
if device.type == "cuda":
|
|
492
|
+
# this can help accelerate GPU compute
|
|
493
|
+
torch.backends.cudnn.benchmark = True
|
|
494
|
+
|
|
495
|
+
if with_dp:
|
|
496
|
+
if isinstance(differential_privacy, DifferentialPrivacyConfig):
|
|
497
|
+
dp_config = differential_privacy.model_dump()
|
|
498
|
+
else:
|
|
499
|
+
dp_config = DifferentialPrivacyConfig(**differential_privacy).model_dump()
|
|
500
|
+
dp_max_epsilon = dp_config.get("max_epsilon") or float("inf")
|
|
501
|
+
dp_delta = dp_config.get("delta", 1e-5)
|
|
502
|
+
# the implementation of PRV accountant seems to have numerical and memory issues for small noise multiplier
|
|
503
|
+
# therefore, we choose RDP instead as it is more stable and provides comparable privacy guarantees
|
|
504
|
+
dp_accountant = "rdp" # hard-coded for now
|
|
505
|
+
_LOG.info(f"{dp_config=}, {dp_accountant=}")
|
|
506
|
+
privacy_engine = PrivacyEngine(accountant=dp_accountant)
|
|
507
|
+
if model_state_strategy == ModelStateStrategy.resume and workspace.model_dp_accountant_path.exists():
|
|
508
|
+
_LOG.info("restore DP accountant state")
|
|
509
|
+
torch.serialization.add_safe_globals([getattr, PRVAccountant, RDPAccountant, GaussianAccountant])
|
|
510
|
+
privacy_engine.accountant.load_state_dict(
|
|
511
|
+
torch.load(workspace.model_dp_accountant_path, map_location=device, weights_only=True),
|
|
512
|
+
)
|
|
513
|
+
# Opacus will return the modified objects
|
|
514
|
+
# - model: wrapped in GradSampleModule and contains additional hooks for computing per-sample gradients
|
|
515
|
+
# - optimizer: wrapped in DPOptimizer and will do different operations during virtual steps and logical steps
|
|
516
|
+
# - dataloader: the dataloader with batch_sampler=UniformWithReplacementSampler (for Poisson sampling)
|
|
517
|
+
model, optimizer, trn_dataloader = privacy_engine.make_private(
|
|
518
|
+
module=model,
|
|
519
|
+
optimizer=optimizer,
|
|
520
|
+
data_loader=trn_dataloader,
|
|
521
|
+
noise_multiplier=dp_config.get("noise_multiplier"),
|
|
522
|
+
max_grad_norm=dp_config.get("max_grad_norm"),
|
|
523
|
+
poisson_sampling=True,
|
|
524
|
+
)
|
|
525
|
+
# this further wraps the dataloader with batch_sampler=BatchSplittingSampler to achieve gradient accumulation
|
|
526
|
+
# it will split the sampled logical batches into smaller sub-batches with batch_size
|
|
527
|
+
trn_dataloader = wrap_data_loader(
|
|
528
|
+
data_loader=trn_dataloader, max_batch_size=batch_size, optimizer=optimizer
|
|
529
|
+
)
|
|
530
|
+
else:
|
|
531
|
+
privacy_engine = None
|
|
532
|
+
dp_config, dp_delta, dp_accountant = None, None, None
|
|
533
|
+
|
|
534
|
+
progress_message = None
|
|
535
|
+
start_trn_time = time.time()
|
|
536
|
+
last_msg_time = time.time()
|
|
537
|
+
trn_data_iter = iter(trn_dataloader)
|
|
538
|
+
do_stop = False
|
|
539
|
+
current_lr = initial_lr
|
|
540
|
+
forward_ctx_mgr = (
|
|
541
|
+
torch.autocast(device_type=device.type, dtype=torch.bfloat16) if use_mixed_precision else nullcontext()
|
|
542
|
+
)
|
|
543
|
+
# infinite loop over training steps, until we decide to stop
|
|
544
|
+
# either because of max_epochs, max_training_time or early_stopping
|
|
545
|
+
while not do_stop:
|
|
546
|
+
is_checkpoint = 0
|
|
547
|
+
steps += 1
|
|
548
|
+
epoch = steps / trn_steps
|
|
549
|
+
|
|
550
|
+
stop_accumulating_grads = False
|
|
551
|
+
accumulated_steps = 0
|
|
552
|
+
if not with_dp:
|
|
553
|
+
optimizer.zero_grad(set_to_none=True)
|
|
554
|
+
while not stop_accumulating_grads:
|
|
555
|
+
# fetch next training (micro)batch
|
|
556
|
+
try:
|
|
557
|
+
step_data = next(trn_data_iter)
|
|
558
|
+
except StopIteration:
|
|
559
|
+
trn_data_iter = iter(trn_dataloader)
|
|
560
|
+
step_data = next(trn_data_iter)
|
|
561
|
+
step_data = {k: v.to(device) for k, v in step_data.items()}
|
|
562
|
+
if with_dp:
|
|
563
|
+
# opacus handles the gradient accumulation internally
|
|
564
|
+
optimizer.zero_grad(set_to_none=True)
|
|
565
|
+
with warnings.catch_warnings():
|
|
566
|
+
# remove this ctx mgr and filter when https://github.com/pytorch/pytorch/issues/130659 is fixed
|
|
567
|
+
warnings.filterwarnings("ignore", category=FutureWarning, message=".*torch.cpu.amp.autocast.*")
|
|
568
|
+
warnings.filterwarnings("ignore", category=FutureWarning, message="Using a non-full backward hook*")
|
|
569
|
+
# forward pass + calculate sample losses
|
|
570
|
+
with forward_ctx_mgr:
|
|
571
|
+
outputs = model(**step_data)
|
|
572
|
+
# FIXME approximation, should be divided by total sum of number of tokens in the batch
|
|
573
|
+
# as in _calculate_per_label_losses, also the final sample may be smaller than the batch size.
|
|
574
|
+
step_loss = outputs.loss / (1 if with_dp else gradient_accumulation_steps)
|
|
575
|
+
step_loss.backward()
|
|
576
|
+
accumulated_steps += 1
|
|
577
|
+
# explicitly count the number of processed samples as the actual batch size can vary when DP is on
|
|
578
|
+
samples += step_data["input_ids"].shape[0]
|
|
579
|
+
if with_dp:
|
|
580
|
+
# for DP training, the optimizer will do different operations during virtual steps and logical steps
|
|
581
|
+
# - virtual step: clip and accumulate gradients
|
|
582
|
+
# - logical step: clip and accumulate gradients, add noises to gradients and update parameters
|
|
583
|
+
optimizer.step()
|
|
584
|
+
# if step was not skipped, it was a logical step, and we can stop accumulating gradients
|
|
585
|
+
stop_accumulating_grads = not optimizer._is_last_step_skipped
|
|
586
|
+
elif accumulated_steps % gradient_accumulation_steps == 0:
|
|
587
|
+
# update parameters with accumulated gradients
|
|
588
|
+
optimizer.step()
|
|
589
|
+
stop_accumulating_grads = True
|
|
590
|
+
current_lr = optimizer.param_groups[0][
|
|
591
|
+
"lr"
|
|
592
|
+
] # currently assume that we have the same lr for all param groups
|
|
593
|
+
# only the scheduling for ReduceLROnPlateau is postponed until the metric becomes available
|
|
594
|
+
if not isinstance(lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
|
|
595
|
+
lr_scheduler.step()
|
|
596
|
+
|
|
597
|
+
# do validation
|
|
598
|
+
do_validation = epoch.is_integer()
|
|
599
|
+
if do_validation:
|
|
600
|
+
# calculate val loss
|
|
601
|
+
with forward_ctx_mgr:
|
|
602
|
+
val_loss = _calculate_val_loss(model=model, val_dataloader=val_dataloader)
|
|
603
|
+
dp_epsilon = privacy_engine.get_epsilon(dp_delta) if with_dp else None
|
|
604
|
+
has_exceeded_dp_max_epsilon = dp_epsilon > dp_max_epsilon if with_dp else False
|
|
605
|
+
# save model weights with the best validation loss (and that hasn't exceeded DP max epsilon)
|
|
606
|
+
if not has_exceeded_dp_max_epsilon:
|
|
607
|
+
is_checkpoint = model_checkpoint.save_checkpoint_if_best(
|
|
608
|
+
val_loss=val_loss,
|
|
609
|
+
model=model,
|
|
610
|
+
optimizer=optimizer,
|
|
611
|
+
lr_scheduler=lr_scheduler,
|
|
612
|
+
dp_accountant=privacy_engine.accountant if with_dp else None,
|
|
613
|
+
)
|
|
614
|
+
else:
|
|
615
|
+
_LOG.info("early stopping: current DP epsilon has exceeded max epsilon")
|
|
616
|
+
# gather message for progress with checkpoint info
|
|
617
|
+
progress_message = ProgressMessage(
|
|
618
|
+
epoch=epoch,
|
|
619
|
+
is_checkpoint=is_checkpoint,
|
|
620
|
+
steps=steps,
|
|
621
|
+
samples=samples,
|
|
622
|
+
trn_loss=None,
|
|
623
|
+
val_loss=val_loss,
|
|
624
|
+
total_time=total_time_init + time.time() - start_trn_time,
|
|
625
|
+
learn_rate=current_lr,
|
|
626
|
+
dp_eps=dp_epsilon,
|
|
627
|
+
dp_delta=dp_delta,
|
|
628
|
+
)
|
|
629
|
+
# check for early stopping
|
|
630
|
+
do_stop = early_stopper(val_loss=val_loss) or has_exceeded_dp_max_epsilon
|
|
631
|
+
# scheduling for ReduceLROnPlateau
|
|
632
|
+
if isinstance(lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
|
|
633
|
+
lr_scheduler.step(metrics=val_loss)
|
|
634
|
+
|
|
635
|
+
# log progress, either by time or by steps, whatever is shorter
|
|
636
|
+
elapsed_training_time = time.time() - start_trn_time
|
|
637
|
+
estimated_time_for_max_epochs = (max_epochs * trn_steps) * (elapsed_training_time / steps)
|
|
638
|
+
if max_training_time < estimated_time_for_max_epochs:
|
|
639
|
+
# use seconds for measuring progress against max_training_time
|
|
640
|
+
progress_total_count = max_training_time
|
|
641
|
+
progress_processed = elapsed_training_time
|
|
642
|
+
else:
|
|
643
|
+
# use steps for measuring progress against max_epochs
|
|
644
|
+
progress_total_count = max_epochs * trn_steps
|
|
645
|
+
progress_processed = steps
|
|
646
|
+
# send a progress message at least every X minutes
|
|
647
|
+
last_msg_interval = 5 * 60
|
|
648
|
+
last_msg_elapsed = time.time() - last_msg_time
|
|
649
|
+
if progress_message is None and (last_msg_elapsed > last_msg_interval or steps == 1):
|
|
650
|
+
dp_epsilon = privacy_engine.get_epsilon(dp_delta) if with_dp else None
|
|
651
|
+
progress_message = ProgressMessage(
|
|
652
|
+
epoch=epoch,
|
|
653
|
+
is_checkpoint=is_checkpoint,
|
|
654
|
+
steps=steps,
|
|
655
|
+
samples=samples,
|
|
656
|
+
trn_loss=None,
|
|
657
|
+
val_loss=None,
|
|
658
|
+
total_time=total_time_init + time.time() - start_trn_time,
|
|
659
|
+
learn_rate=current_lr,
|
|
660
|
+
dp_eps=dp_epsilon,
|
|
661
|
+
dp_delta=dp_delta,
|
|
662
|
+
)
|
|
663
|
+
if progress_message:
|
|
664
|
+
last_msg_time = time.time()
|
|
665
|
+
# send progress update
|
|
666
|
+
res = progress.update(
|
|
667
|
+
completed=int(progress_processed),
|
|
668
|
+
total=int(progress_total_count),
|
|
669
|
+
message=progress_message,
|
|
670
|
+
)
|
|
671
|
+
if do_validation:
|
|
672
|
+
upload_model_data_callback()
|
|
673
|
+
progress_message = None
|
|
674
|
+
if (res or {}).get("stopExecution", False):
|
|
675
|
+
_LOG.info("received STOP EXECUTION signal")
|
|
676
|
+
do_stop = True
|
|
677
|
+
|
|
678
|
+
# check for max_epochs
|
|
679
|
+
if epoch > max_epochs:
|
|
680
|
+
do_stop = True
|
|
681
|
+
|
|
682
|
+
# check for max_training_time
|
|
683
|
+
if (total_time_init + time.time() - start_trn_time) > max_training_time:
|
|
684
|
+
do_stop = True
|
|
685
|
+
|
|
686
|
+
# no checkpoint is saved yet because the training stopped before the first epoch ended
|
|
687
|
+
if not model_checkpoint.has_saved_once():
|
|
688
|
+
_LOG.info("saving model weights, as none were saved so far")
|
|
689
|
+
model_checkpoint.save_checkpoint(
|
|
690
|
+
model=model,
|
|
691
|
+
optimizer=optimizer,
|
|
692
|
+
lr_scheduler=lr_scheduler,
|
|
693
|
+
dp_accountant=privacy_engine.accountant if with_dp else None,
|
|
694
|
+
)
|
|
695
|
+
with forward_ctx_mgr:
|
|
696
|
+
val_loss = _calculate_val_loss(model=model, val_dataloader=val_dataloader)
|
|
697
|
+
dp_epsilon = privacy_engine.get_epsilon(dp_delta) if with_dp else None
|
|
698
|
+
# send a final message to inform how far we've progressed
|
|
699
|
+
progress_message = ProgressMessage(
|
|
700
|
+
epoch=epoch,
|
|
701
|
+
is_checkpoint=1,
|
|
702
|
+
steps=steps,
|
|
703
|
+
samples=samples,
|
|
704
|
+
trn_loss=None,
|
|
705
|
+
val_loss=val_loss,
|
|
706
|
+
total_time=total_time_init + time.time() - start_trn_time,
|
|
707
|
+
learn_rate=current_lr,
|
|
708
|
+
dp_eps=dp_epsilon,
|
|
709
|
+
dp_delta=dp_delta,
|
|
710
|
+
)
|
|
711
|
+
progress.update(
|
|
712
|
+
completed=steps,
|
|
713
|
+
total=steps,
|
|
714
|
+
message=progress_message,
|
|
715
|
+
)
|
|
716
|
+
# ensure everything gets uploaded
|
|
717
|
+
upload_model_data_callback()
|
|
718
|
+
_LOG.info(f"TRAIN_LANGUAGE finished in {time.time() - t0_:.2f}s")
|