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,856 @@
|
|
|
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 logging
|
|
16
|
+
import math
|
|
17
|
+
import time
|
|
18
|
+
import warnings
|
|
19
|
+
from itertools import zip_longest
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
|
|
23
|
+
from importlib.metadata import version
|
|
24
|
+
import numpy as np
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import torch
|
|
27
|
+
from datasets import load_dataset, disable_progress_bar
|
|
28
|
+
from torch.utils.data import DataLoader
|
|
29
|
+
from torch.optim.lr_scheduler import LRScheduler
|
|
30
|
+
|
|
31
|
+
from torch import nn
|
|
32
|
+
|
|
33
|
+
from opacus import PrivacyEngine, GradSampleModule
|
|
34
|
+
from opacus.accountants import PRVAccountant, RDPAccountant, GaussianAccountant
|
|
35
|
+
from opacus.utils.batch_memory_manager import wrap_data_loader
|
|
36
|
+
|
|
37
|
+
from mostlyai.engine._memory import get_available_ram_for_heuristics
|
|
38
|
+
from mostlyai.engine.domain import ModelStateStrategy, DifferentialPrivacyConfig
|
|
39
|
+
from mostlyai.engine._tabular.argn import (
|
|
40
|
+
FlatModel,
|
|
41
|
+
ModelSize,
|
|
42
|
+
SequentialModel,
|
|
43
|
+
get_model_units,
|
|
44
|
+
get_no_of_model_parameters,
|
|
45
|
+
)
|
|
46
|
+
from mostlyai.engine._common import (
|
|
47
|
+
CTXFLT,
|
|
48
|
+
CTXSEQ,
|
|
49
|
+
TGT,
|
|
50
|
+
get_cardinalities,
|
|
51
|
+
get_columns_from_cardinalities,
|
|
52
|
+
get_ctx_sequence_length,
|
|
53
|
+
get_max_data_points_per_sample,
|
|
54
|
+
get_sequence_length_stats,
|
|
55
|
+
get_sub_columns_from_cardinalities,
|
|
56
|
+
get_sub_columns_nested_from_cardinalities,
|
|
57
|
+
SIDX_SUB_COLUMN_PREFIX,
|
|
58
|
+
SLEN_SUB_COLUMN_PREFIX,
|
|
59
|
+
SDEC_SUB_COLUMN_PREFIX,
|
|
60
|
+
ProgressCallback,
|
|
61
|
+
ProgressCallbackWrapper,
|
|
62
|
+
)
|
|
63
|
+
from mostlyai.engine._tabular.common import load_model_weights
|
|
64
|
+
from mostlyai.engine._training_utils import (
|
|
65
|
+
check_early_training_exit,
|
|
66
|
+
EarlyStopper,
|
|
67
|
+
ModelCheckpoint,
|
|
68
|
+
ProgressMessage,
|
|
69
|
+
)
|
|
70
|
+
from mostlyai.engine._workspace import Workspace, ensure_workspace_dir
|
|
71
|
+
|
|
72
|
+
_LOG = logging.getLogger(__name__)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
##################
|
|
76
|
+
### HEURISTICS ###
|
|
77
|
+
##################
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _training_batch_size_heuristic(
|
|
81
|
+
mem_available_gb: float,
|
|
82
|
+
no_of_records: int,
|
|
83
|
+
no_tgt_data_points: int,
|
|
84
|
+
no_ctx_data_points: int,
|
|
85
|
+
no_of_model_params: int,
|
|
86
|
+
) -> tuple[int, int]:
|
|
87
|
+
"""
|
|
88
|
+
Calculate the physical batch size and gradient accumulation steps.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
mem_available_gb (float): Available memory in GB.
|
|
92
|
+
no_of_records (int): Number of records in the training dataset.
|
|
93
|
+
no_tgt_data_points (int): Number of target data points per sample.
|
|
94
|
+
no_ctx_data_points (int): Number of context data points per sample.
|
|
95
|
+
no_of_model_params (int): Number of model parameters.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Tuple[int, int]: A tuple containing:
|
|
99
|
+
- Batch size (int)
|
|
100
|
+
- Gradient accumulation steps (int)
|
|
101
|
+
"""
|
|
102
|
+
data_points = no_tgt_data_points + no_ctx_data_points
|
|
103
|
+
min_batch_size = 8
|
|
104
|
+
# scale batch_size corresponding to available memory
|
|
105
|
+
if mem_available_gb >= 32:
|
|
106
|
+
mem_scale = 2.0
|
|
107
|
+
elif mem_available_gb >= 8:
|
|
108
|
+
mem_scale = 1.0
|
|
109
|
+
else:
|
|
110
|
+
mem_scale = 0.5
|
|
111
|
+
# set max_batch_size corresponding to available memory, model params and data points
|
|
112
|
+
if no_of_model_params > 1_000_000_000 or data_points > 100_000:
|
|
113
|
+
max_batch_size = int(8 * mem_scale)
|
|
114
|
+
elif no_of_model_params > 100_000_000 or data_points > 10_000:
|
|
115
|
+
max_batch_size = int(32 * mem_scale)
|
|
116
|
+
elif no_of_model_params > 10_000_000 or data_points > 1_000:
|
|
117
|
+
max_batch_size = int(128 * mem_scale)
|
|
118
|
+
elif no_of_model_params > 1_000_000 or data_points > 100:
|
|
119
|
+
max_batch_size = int(512 * mem_scale)
|
|
120
|
+
else:
|
|
121
|
+
max_batch_size = int(2048 * mem_scale)
|
|
122
|
+
# ensure a minimum number of batches to avoid excessive padding
|
|
123
|
+
min_batches = 64
|
|
124
|
+
batch_size = 2 ** int(np.log2(no_of_records / min_batches)) if no_of_records > 0 else min_batch_size
|
|
125
|
+
return int(np.clip(a=batch_size, a_min=min_batch_size, a_max=max_batch_size)), 1
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _learn_rate_heuristic(batch_size: int) -> float:
|
|
129
|
+
learn_rate = np.round(0.001 * np.sqrt(batch_size / 32), 5)
|
|
130
|
+
return learn_rate
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
####################
|
|
134
|
+
### DATA LOADERS ###
|
|
135
|
+
####################
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class BatchCollator:
|
|
139
|
+
"""
|
|
140
|
+
Collate a batch of samples into a dictionary of tensors.
|
|
141
|
+
For sequence data, it will sample subsequences with lengths up to max_sequence_window.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(self, is_sequential: bool, max_sequence_window: int | None, device: torch.device):
|
|
145
|
+
self.is_sequential = is_sequential
|
|
146
|
+
self.max_sequence_window = max_sequence_window
|
|
147
|
+
self.device = device
|
|
148
|
+
|
|
149
|
+
def __call__(self, batch: list[dict]) -> dict[str, torch.Tensor]:
|
|
150
|
+
batch = pd.DataFrame(batch)
|
|
151
|
+
if self.is_sequential and self.max_sequence_window:
|
|
152
|
+
batch = self._slice_sequences(batch, self.max_sequence_window)
|
|
153
|
+
batch = self._convert_to_tensors(batch)
|
|
154
|
+
return batch
|
|
155
|
+
|
|
156
|
+
def _convert_to_tensors(self, batch: pd.DataFrame) -> dict[str, torch.Tensor]:
|
|
157
|
+
tensors = {}
|
|
158
|
+
for column in batch.columns:
|
|
159
|
+
if column.startswith(TGT) and self.is_sequential:
|
|
160
|
+
# construct column tensor in single step
|
|
161
|
+
tensors[column] = torch.unsqueeze(
|
|
162
|
+
torch.tensor(
|
|
163
|
+
# pad batch-wise to the longest sequence length with 0s
|
|
164
|
+
np.array(list(zip_longest(*batch[column], fillvalue=0))).T,
|
|
165
|
+
dtype=torch.int64,
|
|
166
|
+
device=self.device,
|
|
167
|
+
),
|
|
168
|
+
dim=-1,
|
|
169
|
+
)
|
|
170
|
+
elif column.startswith(TGT) and not self.is_sequential:
|
|
171
|
+
# construct column tensor in single step
|
|
172
|
+
tensors[column] = torch.unsqueeze(
|
|
173
|
+
torch.tensor(batch[column].values, dtype=torch.int64, device=self.device),
|
|
174
|
+
dim=-1,
|
|
175
|
+
)
|
|
176
|
+
elif column.startswith(CTXFLT):
|
|
177
|
+
# construct column tensor in single step
|
|
178
|
+
tensors[column] = torch.unsqueeze(
|
|
179
|
+
torch.tensor(batch[column].values, dtype=torch.int64, device=self.device),
|
|
180
|
+
dim=-1,
|
|
181
|
+
)
|
|
182
|
+
elif column.startswith(CTXSEQ):
|
|
183
|
+
# construct row tensors and convert the list to nested column tensor
|
|
184
|
+
tensors[column] = torch.unsqueeze(
|
|
185
|
+
torch.nested.as_nested_tensor(
|
|
186
|
+
[torch.tensor(row, dtype=torch.int64, device=self.device) for row in batch[column]],
|
|
187
|
+
dtype=torch.int64,
|
|
188
|
+
device=self.device,
|
|
189
|
+
),
|
|
190
|
+
dim=-1,
|
|
191
|
+
)
|
|
192
|
+
return tensors
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _slice_sequences(batch: pd.DataFrame, max_sequence_window: int) -> pd.DataFrame:
|
|
196
|
+
# determine sequence lengths of current batch
|
|
197
|
+
tgt_columns = [col for col in batch.columns if col.startswith(TGT)]
|
|
198
|
+
seq_lens = batch[tgt_columns[0]].copy().str.len().values
|
|
199
|
+
|
|
200
|
+
# determine sampling logic for current batch
|
|
201
|
+
flip = np.random.random()
|
|
202
|
+
if flip < 0.3: # 30%
|
|
203
|
+
# pick start of the sequence to focus on the beginning
|
|
204
|
+
sel_idxs = [np.arange(0, min(max_sequence_window, seq_len)) for seq_len in seq_lens]
|
|
205
|
+
elif 0.3 <= flip < 0.4: # 10%
|
|
206
|
+
# pick end of the sequence to focus on the end
|
|
207
|
+
sel_idxs = [np.arange(max(0, seq_len - max_sequence_window), seq_len) for seq_len in seq_lens]
|
|
208
|
+
else: # 60%
|
|
209
|
+
# random continuous window to focus on any part
|
|
210
|
+
start_idxs = np.random.randint(low=1 - max_sequence_window, high=seq_lens, size=len(seq_lens))
|
|
211
|
+
# ensure that sequences that fit into max_sequence_length are completely covered
|
|
212
|
+
start_idxs[seq_lens <= max_sequence_window] = 0
|
|
213
|
+
# calculate final start and end indexes
|
|
214
|
+
end_idxs = start_idxs + max_sequence_window
|
|
215
|
+
start_idxs = np.maximum(0, start_idxs)
|
|
216
|
+
sel_idxs = [
|
|
217
|
+
np.arange(start, min(seq_len, end)) for start, end, seq_len in zip(start_idxs, end_idxs, seq_lens)
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
# loop over each record within batch and pick values for each tgt column
|
|
221
|
+
tgt_col_idxs = [batch.columns.get_loc(c) for c in tgt_columns]
|
|
222
|
+
rows = []
|
|
223
|
+
for row_idx, batch_row in enumerate(batch.itertuples(index=False)):
|
|
224
|
+
cells = []
|
|
225
|
+
for col_idx, batch_cell in enumerate(batch_row):
|
|
226
|
+
if col_idx in tgt_col_idxs:
|
|
227
|
+
cells.append([batch_cell[i] for i in sel_idxs[row_idx]])
|
|
228
|
+
else:
|
|
229
|
+
cells.append(batch_cell)
|
|
230
|
+
rows.append(cells)
|
|
231
|
+
|
|
232
|
+
return pd.DataFrame(rows, columns=batch.columns, index=batch.index)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
#####################
|
|
236
|
+
### TRAINING LOOP ###
|
|
237
|
+
#####################
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class TabularModelCheckpoint(ModelCheckpoint):
|
|
241
|
+
def _save_model_weights(self, model: torch.nn.Module):
|
|
242
|
+
if isinstance(model, GradSampleModule):
|
|
243
|
+
state_dict = model._module.state_dict()
|
|
244
|
+
else:
|
|
245
|
+
state_dict = model.state_dict()
|
|
246
|
+
torch.save(state_dict, self.workspace.model_tabular_weights_path)
|
|
247
|
+
|
|
248
|
+
def _clear_model_weights(self) -> None:
|
|
249
|
+
self.workspace.model_tabular_weights_path.unlink(missing_ok=True)
|
|
250
|
+
|
|
251
|
+
def model_weights_path_exists(self) -> bool:
|
|
252
|
+
return self.workspace.model_tabular_weights_path.exists()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _calculate_sample_losses(
|
|
256
|
+
model: FlatModel | SequentialModel | GradSampleModule, data: dict[str, torch.Tensor]
|
|
257
|
+
) -> torch.Tensor:
|
|
258
|
+
with warnings.catch_warnings():
|
|
259
|
+
warnings.filterwarnings("ignore", category=FutureWarning, message="Using a non-full backward hook*")
|
|
260
|
+
output, _ = model(data, mode="trn")
|
|
261
|
+
criterion = nn.CrossEntropyLoss(reduction="none")
|
|
262
|
+
|
|
263
|
+
tgt_cols = (
|
|
264
|
+
list(model.tgt_cardinalities.keys())
|
|
265
|
+
if not isinstance(model, GradSampleModule)
|
|
266
|
+
else model._module.tgt_cardinalities.keys()
|
|
267
|
+
)
|
|
268
|
+
if isinstance(model, SequentialModel) or (
|
|
269
|
+
isinstance(model, GradSampleModule) and isinstance(model._module, SequentialModel)
|
|
270
|
+
):
|
|
271
|
+
slen_cols = [k for k in data if k.startswith(SLEN_SUB_COLUMN_PREFIX)]
|
|
272
|
+
|
|
273
|
+
# generate masks for SLEN and time step
|
|
274
|
+
slen_mask = torch.zeros_like(data[slen_cols[0]], dtype=torch.int64)
|
|
275
|
+
for slen_col in slen_cols:
|
|
276
|
+
slen_mask |= data[slen_col] != 0 # mask loss for padded rows, which have SLEN=0
|
|
277
|
+
slen_mask = slen_mask.squeeze(-1)
|
|
278
|
+
time_step_mask = torch.zeros_like(slen_mask, dtype=torch.int64)
|
|
279
|
+
time_step_mask[:, 0] = 10 # mask loss for all time steps except the first one, and emphasize that one by 10x
|
|
280
|
+
|
|
281
|
+
# calculate per column losses
|
|
282
|
+
sidx_cols = {k for k in data if k.startswith(SIDX_SUB_COLUMN_PREFIX)}
|
|
283
|
+
sdec_cols = {k for k in data if k.startswith(SDEC_SUB_COLUMN_PREFIX)}
|
|
284
|
+
losses_by_column = []
|
|
285
|
+
for col in tgt_cols:
|
|
286
|
+
if col in slen_cols:
|
|
287
|
+
# mask out SLEN for steps > 1
|
|
288
|
+
mask = time_step_mask
|
|
289
|
+
elif col in sidx_cols or col in sdec_cols:
|
|
290
|
+
# SIDX and SDEC columns need to be present in the computation graph for DP to work
|
|
291
|
+
# so we're only masking them instead of skipping them completely
|
|
292
|
+
mask = torch.zeros_like(slen_mask, dtype=torch.int64)
|
|
293
|
+
else:
|
|
294
|
+
# mask out paddings
|
|
295
|
+
mask = slen_mask
|
|
296
|
+
|
|
297
|
+
column_loss = criterion(output[col].transpose(1, 2), data[col].squeeze(2))
|
|
298
|
+
masked_loss = torch.sum(column_loss * mask, dim=1) / torch.clamp(torch.sum(mask, dim=1), min=1)
|
|
299
|
+
losses_by_column.append(masked_loss)
|
|
300
|
+
else:
|
|
301
|
+
losses_by_column = [criterion(output[col], data[col].squeeze(1)) for col in tgt_cols]
|
|
302
|
+
# sum up column level losses to get overall losses at sample level
|
|
303
|
+
losses = torch.sum(torch.stack(losses_by_column, dim=0), dim=0)
|
|
304
|
+
return losses
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# gradient tracking is not needed for validation steps, disable it to save memory
|
|
308
|
+
@torch.no_grad()
|
|
309
|
+
def _calculate_val_loss(
|
|
310
|
+
model: FlatModel | SequentialModel,
|
|
311
|
+
val_dataloader: DataLoader,
|
|
312
|
+
) -> float:
|
|
313
|
+
val_sample_losses: list[torch.Tensor] = []
|
|
314
|
+
model.eval()
|
|
315
|
+
for step_data in val_dataloader:
|
|
316
|
+
step_losses = _calculate_sample_losses(model, step_data)
|
|
317
|
+
val_sample_losses.extend(step_losses.detach())
|
|
318
|
+
model.train()
|
|
319
|
+
val_sample_losses: torch.Tensor = torch.stack(val_sample_losses, dim=0)
|
|
320
|
+
val_loss_avg = torch.mean(val_sample_losses).item()
|
|
321
|
+
return val_loss_avg
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _calculate_average_trn_loss(trn_sample_losses: list[torch.Tensor], n: int | None = None) -> float | None:
|
|
325
|
+
if len(trn_sample_losses) == 0:
|
|
326
|
+
return None
|
|
327
|
+
trn_losses_latest = torch.stack(trn_sample_losses, dim=0)
|
|
328
|
+
if n is not None:
|
|
329
|
+
trn_losses_latest = trn_losses_latest[-n:]
|
|
330
|
+
trn_loss = torch.mean(trn_losses_latest).item()
|
|
331
|
+
return trn_loss
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
################
|
|
335
|
+
### TRAINING ###
|
|
336
|
+
################
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def train(
|
|
340
|
+
*,
|
|
341
|
+
model: str = "MOSTLY_AI/Medium",
|
|
342
|
+
max_training_time: float = 60.0,
|
|
343
|
+
max_epochs: float = 100.0,
|
|
344
|
+
batch_size: int | None = None,
|
|
345
|
+
gradient_accumulation_steps: int | None = None,
|
|
346
|
+
max_sequence_window: int = 100,
|
|
347
|
+
enable_flexible_generation: bool = True,
|
|
348
|
+
differential_privacy: DifferentialPrivacyConfig | dict | None = None,
|
|
349
|
+
upload_model_data_callback: Callable | None = None,
|
|
350
|
+
model_state_strategy: ModelStateStrategy | str = ModelStateStrategy.reset,
|
|
351
|
+
device: torch.device | str | None = None,
|
|
352
|
+
workspace_dir: str | Path = "engine-ws",
|
|
353
|
+
update_progress: ProgressCallback | None = None,
|
|
354
|
+
):
|
|
355
|
+
_LOG.info("TRAIN_TABULAR started")
|
|
356
|
+
t0 = time.time()
|
|
357
|
+
workspace_dir = ensure_workspace_dir(workspace_dir)
|
|
358
|
+
workspace = Workspace(workspace_dir)
|
|
359
|
+
with ProgressCallbackWrapper(
|
|
360
|
+
update_progress, progress_messages_path=workspace.model_progress_messages_path
|
|
361
|
+
) as progress:
|
|
362
|
+
_LOG.info(f"numpy={version('numpy')}, pandas={version('pandas')}")
|
|
363
|
+
_LOG.info(f"torch={version('torch')}, opacus={version('opacus')}")
|
|
364
|
+
device = (
|
|
365
|
+
torch.device(device)
|
|
366
|
+
if device is not None
|
|
367
|
+
else (torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu"))
|
|
368
|
+
)
|
|
369
|
+
_LOG.info(f"{device=}")
|
|
370
|
+
torch.set_default_dtype(torch.float32)
|
|
371
|
+
|
|
372
|
+
has_context = workspace.ctx_stats.path.exists()
|
|
373
|
+
tgt_stats = workspace.tgt_stats.read()
|
|
374
|
+
ctx_stats = workspace.ctx_stats.read()
|
|
375
|
+
is_sequential = tgt_stats["is_sequential"]
|
|
376
|
+
_LOG.info(f"{is_sequential=}")
|
|
377
|
+
trn_cnt = tgt_stats["no_of_training_records"]
|
|
378
|
+
val_cnt = tgt_stats["no_of_validation_records"]
|
|
379
|
+
tgt_cardinalities = get_cardinalities(tgt_stats)
|
|
380
|
+
ctx_cardinalities = get_cardinalities(ctx_stats) if has_context else {}
|
|
381
|
+
tgt_sub_columns = get_sub_columns_from_cardinalities(tgt_cardinalities)
|
|
382
|
+
ctx_nested_sub_columns = get_sub_columns_nested_from_cardinalities(ctx_cardinalities, "processor")
|
|
383
|
+
ctxflt_sub_columns = ctx_nested_sub_columns.get(CTXFLT, [])
|
|
384
|
+
ctxseq_sub_columns = ctx_nested_sub_columns.get(CTXSEQ, [])
|
|
385
|
+
|
|
386
|
+
# set defaults
|
|
387
|
+
max_training_time = max(0.0, max_training_time) * 60 # convert to seconds
|
|
388
|
+
_LOG.info(f"{max_training_time=}s")
|
|
389
|
+
max_epochs = max(0.0, max_epochs)
|
|
390
|
+
max_epochs_cap = math.ceil((trn_cnt + val_cnt) / 50)
|
|
391
|
+
if max_epochs_cap < max_epochs:
|
|
392
|
+
_LOG.info(f"{max_epochs=} -> max_epochs={max_epochs_cap} due to small sample size")
|
|
393
|
+
max_epochs = max_epochs_cap
|
|
394
|
+
else:
|
|
395
|
+
_LOG.info(f"{max_epochs=}")
|
|
396
|
+
model_sizes = {
|
|
397
|
+
"MOSTLY_AI/Small": ModelSize.S,
|
|
398
|
+
"MOSTLY_AI/Medium": ModelSize.M,
|
|
399
|
+
"MOSTLY_AI/Large": ModelSize.L,
|
|
400
|
+
}
|
|
401
|
+
if model not in model_sizes:
|
|
402
|
+
raise ValueError(f"model {model} not supported")
|
|
403
|
+
model_size = model_sizes[model]
|
|
404
|
+
_LOG.info(f"{model_size=}")
|
|
405
|
+
_LOG.info(f"{enable_flexible_generation=}")
|
|
406
|
+
with_dp = differential_privacy is not None
|
|
407
|
+
_LOG.info(f"{with_dp=}")
|
|
408
|
+
_LOG.info(f"{model_state_strategy=}")
|
|
409
|
+
|
|
410
|
+
# initialize callbacks
|
|
411
|
+
upload_model_data_callback = upload_model_data_callback or (lambda *args, **kwargs: None)
|
|
412
|
+
|
|
413
|
+
# early exit if there is not enough data to train the model
|
|
414
|
+
# in such scenario, training model is not created
|
|
415
|
+
# and weights are not stored, so generation must be resilient to that
|
|
416
|
+
if check_early_training_exit(workspace=workspace, trn_cnt=trn_cnt, val_cnt=val_cnt):
|
|
417
|
+
_LOG.warning("not enough data to train model; skipping training")
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
# determine column order for training
|
|
421
|
+
if enable_flexible_generation:
|
|
422
|
+
# random column order for each batch
|
|
423
|
+
trn_column_order = None
|
|
424
|
+
else:
|
|
425
|
+
# fixed column order based on cardinalities
|
|
426
|
+
tgt_cardinalities = get_cardinalities(tgt_stats)
|
|
427
|
+
trn_column_order = get_columns_from_cardinalities(tgt_cardinalities)
|
|
428
|
+
|
|
429
|
+
# gather sequence length stats for heuristics
|
|
430
|
+
tgt_seq_len_stats = get_sequence_length_stats(tgt_stats)
|
|
431
|
+
tgt_seq_len_deciles = tgt_seq_len_stats["deciles"]
|
|
432
|
+
tgt_seq_len_median = tgt_seq_len_stats["median"]
|
|
433
|
+
tgt_seq_len_max = tgt_seq_len_stats["max"]
|
|
434
|
+
max_sequence_window = np.clip(max_sequence_window, a_min=1, a_max=tgt_seq_len_max)
|
|
435
|
+
_LOG.info(f"{max_sequence_window=}")
|
|
436
|
+
_LOG.info(f"{tgt_seq_len_deciles=}")
|
|
437
|
+
ctx_seq_deciles = get_ctx_sequence_length(ctx_stats, key="deciles")
|
|
438
|
+
ctx_seq_len_median = get_ctx_sequence_length(ctx_stats, key="median")
|
|
439
|
+
_LOG.info(f"{ctx_seq_deciles=}")
|
|
440
|
+
|
|
441
|
+
# the line below fixes issue with growing epoch time for later epochs
|
|
442
|
+
# https://discuss.pytorch.org/t/training-time-gets-slower-and-slower-on-cpu/145483
|
|
443
|
+
torch.set_flush_denormal(True)
|
|
444
|
+
|
|
445
|
+
_LOG.info("create training model")
|
|
446
|
+
model_checkpoint = TabularModelCheckpoint(workspace=workspace)
|
|
447
|
+
argn: SequentialModel | FlatModel
|
|
448
|
+
if is_sequential:
|
|
449
|
+
argn = SequentialModel(
|
|
450
|
+
tgt_cardinalities=tgt_cardinalities,
|
|
451
|
+
ctx_cardinalities=ctx_cardinalities,
|
|
452
|
+
tgt_seq_len_median=tgt_seq_len_median,
|
|
453
|
+
tgt_seq_len_max=tgt_seq_len_max,
|
|
454
|
+
ctxseq_len_median=ctx_seq_len_median,
|
|
455
|
+
model_size=model_size,
|
|
456
|
+
column_order=trn_column_order,
|
|
457
|
+
device=device,
|
|
458
|
+
with_dp=with_dp, # this flag decides whether the model is initialized with LSTM or DPLSTM layers
|
|
459
|
+
)
|
|
460
|
+
else:
|
|
461
|
+
argn = FlatModel(
|
|
462
|
+
tgt_cardinalities=tgt_cardinalities,
|
|
463
|
+
ctx_cardinalities=ctx_cardinalities,
|
|
464
|
+
ctxseq_len_median=ctx_seq_len_median,
|
|
465
|
+
model_size=model_size,
|
|
466
|
+
column_order=trn_column_order,
|
|
467
|
+
device=device,
|
|
468
|
+
)
|
|
469
|
+
_LOG.info(f"model class: {argn.__class__.__name__}")
|
|
470
|
+
|
|
471
|
+
if isinstance(model_state_strategy, str):
|
|
472
|
+
model_state_strategy = ModelStateStrategy(model_state_strategy)
|
|
473
|
+
if not model_checkpoint.model_weights_path_exists():
|
|
474
|
+
_LOG.info(f"model weights not found; change strategy from {model_state_strategy} to RESET")
|
|
475
|
+
model_state_strategy = ModelStateStrategy.reset
|
|
476
|
+
_LOG.info(f"{model_state_strategy=}")
|
|
477
|
+
if model_state_strategy in [ModelStateStrategy.resume, ModelStateStrategy.reuse]:
|
|
478
|
+
_LOG.info("load existing model weights")
|
|
479
|
+
torch.serialization.add_safe_globals([np.core.multiarray.scalar, np.dtype, np.dtypes.Float64DType])
|
|
480
|
+
load_model_weights(model=argn, path=workspace.model_tabular_weights_path, device=device)
|
|
481
|
+
else: # ModelStateStrategy.reset
|
|
482
|
+
_LOG.info("remove existing checkpoint files")
|
|
483
|
+
model_checkpoint.clear_checkpoint()
|
|
484
|
+
|
|
485
|
+
# check how to handle existing progress state
|
|
486
|
+
last_progress_message = progress.get_last_progress_message()
|
|
487
|
+
if last_progress_message and model_state_strategy == ModelStateStrategy.resume:
|
|
488
|
+
epoch = last_progress_message.get("epoch", 0.0)
|
|
489
|
+
steps = last_progress_message.get("steps", 0)
|
|
490
|
+
samples = last_progress_message.get("samples", 0)
|
|
491
|
+
initial_lr = last_progress_message.get("learn_rate", None)
|
|
492
|
+
total_time_init = last_progress_message.get("total_time", 0.0)
|
|
493
|
+
else:
|
|
494
|
+
epoch = 0.0
|
|
495
|
+
steps = 0
|
|
496
|
+
samples = 0
|
|
497
|
+
initial_lr = None
|
|
498
|
+
total_time_init = 0.0
|
|
499
|
+
progress.reset_progress_messages()
|
|
500
|
+
_LOG.info(f"start training progress from {epoch=}, {steps=}")
|
|
501
|
+
|
|
502
|
+
argn.to(device)
|
|
503
|
+
no_of_model_params = get_no_of_model_parameters(argn)
|
|
504
|
+
_LOG.info(f"{no_of_model_params=}")
|
|
505
|
+
|
|
506
|
+
# persist model configs
|
|
507
|
+
model_units = get_model_units(argn)
|
|
508
|
+
model_configs = {
|
|
509
|
+
"model_id": model,
|
|
510
|
+
"model_units": model_units,
|
|
511
|
+
"enable_flexible_generation": enable_flexible_generation,
|
|
512
|
+
}
|
|
513
|
+
workspace.model_tabular_configs.write(model_configs)
|
|
514
|
+
|
|
515
|
+
# heuristics for batch_size and for initial learn_rate
|
|
516
|
+
mem_available_gb = get_available_ram_for_heuristics() / 1024**3
|
|
517
|
+
no_tgt_data_points = get_max_data_points_per_sample(tgt_stats)
|
|
518
|
+
no_ctx_data_points = get_max_data_points_per_sample(ctx_stats)
|
|
519
|
+
default_batch_size, default_gradient_accumulation_steps = _training_batch_size_heuristic(
|
|
520
|
+
mem_available_gb=mem_available_gb,
|
|
521
|
+
no_of_records=trn_cnt,
|
|
522
|
+
no_tgt_data_points=no_tgt_data_points,
|
|
523
|
+
no_ctx_data_points=no_ctx_data_points,
|
|
524
|
+
no_of_model_params=no_of_model_params,
|
|
525
|
+
)
|
|
526
|
+
if batch_size is None:
|
|
527
|
+
batch_size = default_batch_size
|
|
528
|
+
if gradient_accumulation_steps is None:
|
|
529
|
+
gradient_accumulation_steps = default_gradient_accumulation_steps
|
|
530
|
+
|
|
531
|
+
# setup params for input pipeline
|
|
532
|
+
batch_size = max(1, min(batch_size, trn_cnt))
|
|
533
|
+
gradient_accumulation_steps = max(1, min(gradient_accumulation_steps, trn_cnt // batch_size))
|
|
534
|
+
trn_batch_size = batch_size * gradient_accumulation_steps
|
|
535
|
+
trn_steps = max(1, trn_cnt // trn_batch_size)
|
|
536
|
+
val_batch_size = max(1, min(batch_size, val_cnt))
|
|
537
|
+
val_steps = max(1, val_cnt // val_batch_size)
|
|
538
|
+
|
|
539
|
+
if initial_lr is None:
|
|
540
|
+
initial_lr = _learn_rate_heuristic(trn_batch_size)
|
|
541
|
+
if is_sequential:
|
|
542
|
+
# reduce val_batch_size to reduce padding for validation batches,
|
|
543
|
+
# which speeds up compute, plus it results in a more stable val_loss
|
|
544
|
+
val_batch_size = val_batch_size // 2
|
|
545
|
+
|
|
546
|
+
# and see if it's possible to make it compatible with DP
|
|
547
|
+
batch_collator = BatchCollator(
|
|
548
|
+
is_sequential=is_sequential, max_sequence_window=max_sequence_window, device=device
|
|
549
|
+
)
|
|
550
|
+
disable_progress_bar()
|
|
551
|
+
trn_dataset = load_dataset("parquet", data_files=[str(p) for p in workspace.encoded_data_trn.fetch_all()])[
|
|
552
|
+
"train"
|
|
553
|
+
]
|
|
554
|
+
trn_dataloader = DataLoader(
|
|
555
|
+
dataset=trn_dataset,
|
|
556
|
+
shuffle=True,
|
|
557
|
+
# either DP logical batch size or grad accumulation physical batch size
|
|
558
|
+
batch_size=trn_batch_size if with_dp else batch_size,
|
|
559
|
+
collate_fn=batch_collator,
|
|
560
|
+
)
|
|
561
|
+
val_dataset = load_dataset("parquet", data_files=[str(p) for p in workspace.encoded_data_val.fetch_all()])[
|
|
562
|
+
"train"
|
|
563
|
+
]
|
|
564
|
+
val_dataloader = DataLoader(
|
|
565
|
+
dataset=val_dataset,
|
|
566
|
+
shuffle=False,
|
|
567
|
+
batch_size=val_batch_size,
|
|
568
|
+
collate_fn=batch_collator,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
_LOG.info(f"{trn_cnt=}, {val_cnt=}")
|
|
572
|
+
_LOG.info(f"{len(tgt_sub_columns)=}, {len(ctxflt_sub_columns)=}, {len(ctxseq_sub_columns)=}")
|
|
573
|
+
if len(tgt_cardinalities) > 0:
|
|
574
|
+
tgt_cardinalities_deciles = list(
|
|
575
|
+
np.quantile(
|
|
576
|
+
list(tgt_cardinalities.values()),
|
|
577
|
+
np.arange(0, 1.1, 0.1),
|
|
578
|
+
method="lower",
|
|
579
|
+
)
|
|
580
|
+
)
|
|
581
|
+
_LOG.info(f"{tgt_cardinalities_deciles=}")
|
|
582
|
+
if len(ctx_cardinalities) > 0:
|
|
583
|
+
ctx_cardinalities_deciles = list(
|
|
584
|
+
np.quantile(
|
|
585
|
+
list(ctx_cardinalities.values()),
|
|
586
|
+
np.arange(0, 1.1, 0.1),
|
|
587
|
+
method="lower",
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
_LOG.info(f"{ctx_cardinalities_deciles=}")
|
|
591
|
+
_LOG.info(f"{trn_batch_size=}, {val_batch_size=}")
|
|
592
|
+
_LOG.info(f"{trn_steps=}, {val_steps=}")
|
|
593
|
+
_LOG.info(f"{batch_size=}, {gradient_accumulation_steps=}, {initial_lr=}")
|
|
594
|
+
|
|
595
|
+
early_stopper = EarlyStopper(val_loss_patience=4)
|
|
596
|
+
optimizer = torch.optim.AdamW(params=argn.parameters(), lr=initial_lr)
|
|
597
|
+
lr_scheduler: LRScheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
|
|
598
|
+
optimizer=optimizer,
|
|
599
|
+
factor=0.5,
|
|
600
|
+
patience=2,
|
|
601
|
+
min_lr=0.1 * initial_lr,
|
|
602
|
+
# threshold=0, # if we prefer to completely mimic the behavior of previous implementation
|
|
603
|
+
)
|
|
604
|
+
if (
|
|
605
|
+
model_state_strategy == ModelStateStrategy.resume
|
|
606
|
+
and model_checkpoint.optimizer_and_lr_scheduler_paths_exist()
|
|
607
|
+
):
|
|
608
|
+
# restore the full states of optimizer and lr_scheduler when possible
|
|
609
|
+
# otherwise, only the learning rate from the last progress message will be restored
|
|
610
|
+
_LOG.info("restore optimizer and LR scheduler states")
|
|
611
|
+
optimizer.load_state_dict(
|
|
612
|
+
torch.load(workspace.model_optimizer_path, map_location=device, weights_only=True)
|
|
613
|
+
)
|
|
614
|
+
lr_scheduler.load_state_dict(
|
|
615
|
+
torch.load(workspace.model_lr_scheduler_path, map_location=device, weights_only=True)
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
if device.type == "cuda":
|
|
619
|
+
# this can help accelerate GPU compute
|
|
620
|
+
torch.backends.cudnn.benchmark = True
|
|
621
|
+
|
|
622
|
+
if with_dp:
|
|
623
|
+
if isinstance(differential_privacy, DifferentialPrivacyConfig):
|
|
624
|
+
dp_config = differential_privacy.model_dump()
|
|
625
|
+
else:
|
|
626
|
+
dp_config = DifferentialPrivacyConfig(**differential_privacy).model_dump()
|
|
627
|
+
dp_max_epsilon = dp_config.get("max_epsilon") or float("inf")
|
|
628
|
+
dp_delta = dp_config.get("delta", 1e-5)
|
|
629
|
+
# the implementation of PRV accountant seems to have numerical and memory issues for small noise multiplier
|
|
630
|
+
# therefore, we choose RDP instead as it is more stable and provides comparable privacy guarantees
|
|
631
|
+
dp_accountant = "rdp" # hard-coded for now
|
|
632
|
+
_LOG.info(f"{dp_config=}, {dp_accountant=}")
|
|
633
|
+
privacy_engine = PrivacyEngine(accountant=dp_accountant)
|
|
634
|
+
if model_state_strategy == ModelStateStrategy.resume and workspace.model_dp_accountant_path.exists():
|
|
635
|
+
_LOG.info("restore DP accountant state")
|
|
636
|
+
torch.serialization.add_safe_globals([getattr, PRVAccountant, RDPAccountant, GaussianAccountant])
|
|
637
|
+
privacy_engine.accountant.load_state_dict(
|
|
638
|
+
torch.load(workspace.model_dp_accountant_path, map_location=device, weights_only=True)
|
|
639
|
+
)
|
|
640
|
+
# Opacus will return the modified objects
|
|
641
|
+
# - model: wrapped in GradSampleModule and contains additional hooks for computing per-sample gradients
|
|
642
|
+
# - optimizer: wrapped in DPOptimizer and will do different operations during virtual steps and logical steps
|
|
643
|
+
# - dataloader: the dataloader with batch_sampler=UniformWithReplacementSampler (for Poisson sampling)
|
|
644
|
+
argn, optimizer, trn_dataloader = privacy_engine.make_private(
|
|
645
|
+
module=argn,
|
|
646
|
+
optimizer=optimizer,
|
|
647
|
+
data_loader=trn_dataloader,
|
|
648
|
+
noise_multiplier=dp_config.get("noise_multiplier"),
|
|
649
|
+
max_grad_norm=dp_config.get("max_grad_norm"),
|
|
650
|
+
poisson_sampling=True,
|
|
651
|
+
)
|
|
652
|
+
# this further wraps the dataloader with batch_sampler=BatchSplittingSampler to achieve gradient accumulation
|
|
653
|
+
# it will split the sampled logical batches into smaller sub-batches with batch_size
|
|
654
|
+
trn_dataloader = wrap_data_loader(
|
|
655
|
+
data_loader=trn_dataloader, max_batch_size=batch_size, optimizer=optimizer
|
|
656
|
+
)
|
|
657
|
+
else:
|
|
658
|
+
privacy_engine = None
|
|
659
|
+
dp_config, dp_delta, dp_accountant = None, None, None
|
|
660
|
+
|
|
661
|
+
progress_message = None
|
|
662
|
+
start_trn_time = time.time()
|
|
663
|
+
last_msg_time = time.time()
|
|
664
|
+
trn_data_iter = iter(trn_dataloader)
|
|
665
|
+
trn_sample_losses: list[torch.Tensor] = []
|
|
666
|
+
do_stop = False
|
|
667
|
+
current_lr = initial_lr
|
|
668
|
+
# infinite loop over training steps, until we decide to stop
|
|
669
|
+
# either because of max_epochs, max_training_time or early_stopping
|
|
670
|
+
while not do_stop:
|
|
671
|
+
is_checkpoint = 0
|
|
672
|
+
steps += 1
|
|
673
|
+
epoch = steps / trn_steps
|
|
674
|
+
|
|
675
|
+
stop_accumulating_grads = False
|
|
676
|
+
accumulated_steps = 0
|
|
677
|
+
if not with_dp:
|
|
678
|
+
optimizer.zero_grad(set_to_none=True)
|
|
679
|
+
while not stop_accumulating_grads:
|
|
680
|
+
# fetch next training (micro)batch
|
|
681
|
+
try:
|
|
682
|
+
step_data = next(trn_data_iter)
|
|
683
|
+
except StopIteration:
|
|
684
|
+
trn_data_iter = iter(trn_dataloader)
|
|
685
|
+
step_data = next(trn_data_iter)
|
|
686
|
+
# forward pass + calculate sample losses
|
|
687
|
+
step_losses = _calculate_sample_losses(argn, step_data)
|
|
688
|
+
# FIXME in sequential case, this is an approximation, it should be divided by total sum of masks in the
|
|
689
|
+
# entire batch to get the average loss per sample. Less importantly the final sample may be smaller
|
|
690
|
+
# than the batch size in both flat and sequential case.
|
|
691
|
+
# calculate total step loss
|
|
692
|
+
step_loss = torch.mean(step_losses) / (1 if with_dp else gradient_accumulation_steps)
|
|
693
|
+
if with_dp:
|
|
694
|
+
# opacus handles the gradient accumulation internally
|
|
695
|
+
optimizer.zero_grad(set_to_none=True)
|
|
696
|
+
# backward pass
|
|
697
|
+
with warnings.catch_warnings():
|
|
698
|
+
warnings.filterwarnings("ignore", category=FutureWarning, message="Using a non-full backward hook*")
|
|
699
|
+
step_loss.backward()
|
|
700
|
+
accumulated_steps += 1
|
|
701
|
+
# explicitly count the number of processed samples as the actual batch size can vary when DP is on
|
|
702
|
+
samples += step_losses.shape[0]
|
|
703
|
+
if with_dp:
|
|
704
|
+
# for DP training, the optimizer will do different operations during virtual steps and logical steps
|
|
705
|
+
# - virtual step: clip and accumulate gradients
|
|
706
|
+
# - logical step: clip and accumulate gradients, add noises to gradients and update parameters
|
|
707
|
+
optimizer.step()
|
|
708
|
+
# if step was not skipped, it was a logical step, and we can stop accumulating gradients
|
|
709
|
+
stop_accumulating_grads = not optimizer._is_last_step_skipped
|
|
710
|
+
elif accumulated_steps % gradient_accumulation_steps == 0:
|
|
711
|
+
# update parameters with accumulated gradients
|
|
712
|
+
optimizer.step()
|
|
713
|
+
stop_accumulating_grads = True
|
|
714
|
+
# detach losses from the graph
|
|
715
|
+
step_losses = step_losses.detach()
|
|
716
|
+
trn_sample_losses.extend(step_losses)
|
|
717
|
+
|
|
718
|
+
current_lr = optimizer.param_groups[0][
|
|
719
|
+
"lr"
|
|
720
|
+
] # currently assume that we have the same lr for all param groups
|
|
721
|
+
|
|
722
|
+
# only the scheduling for ReduceLROnPlateau is postponed until the metric becomes available
|
|
723
|
+
if not isinstance(lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
|
|
724
|
+
lr_scheduler.step()
|
|
725
|
+
|
|
726
|
+
# do validation
|
|
727
|
+
do_validation = on_epoch_end = epoch.is_integer()
|
|
728
|
+
if do_validation:
|
|
729
|
+
# calculate val loss and trn loss
|
|
730
|
+
val_loss = _calculate_val_loss(model=argn, val_dataloader=val_dataloader)
|
|
731
|
+
# handle scenario where model training ran into numeric instability
|
|
732
|
+
if pd.isna(val_loss):
|
|
733
|
+
_LOG.warning("validation loss is not available - reset model weights to last checkpoint")
|
|
734
|
+
load_model_weights(
|
|
735
|
+
model=argn,
|
|
736
|
+
path=workspace.model_tabular_weights_path,
|
|
737
|
+
device=device,
|
|
738
|
+
)
|
|
739
|
+
trn_loss = _calculate_average_trn_loss(trn_sample_losses)
|
|
740
|
+
dp_epsilon = privacy_engine.get_epsilon(dp_delta) if with_dp else None
|
|
741
|
+
has_exceeded_dp_max_epsilon = dp_epsilon > dp_max_epsilon if with_dp else False
|
|
742
|
+
if not has_exceeded_dp_max_epsilon:
|
|
743
|
+
# save model weights with the best validation loss (and that hasn't exceeded DP max epsilon)
|
|
744
|
+
is_checkpoint = model_checkpoint.save_checkpoint_if_best(
|
|
745
|
+
val_loss=val_loss,
|
|
746
|
+
model=argn,
|
|
747
|
+
optimizer=optimizer,
|
|
748
|
+
lr_scheduler=lr_scheduler,
|
|
749
|
+
dp_accountant=privacy_engine.accountant if with_dp else None,
|
|
750
|
+
)
|
|
751
|
+
else:
|
|
752
|
+
_LOG.info("early stopping: current DP epsilon has exceeded max epsilon")
|
|
753
|
+
# gather message for progress with checkpoint info
|
|
754
|
+
progress_message = ProgressMessage(
|
|
755
|
+
epoch=epoch,
|
|
756
|
+
is_checkpoint=is_checkpoint,
|
|
757
|
+
steps=steps,
|
|
758
|
+
samples=samples,
|
|
759
|
+
trn_loss=trn_loss,
|
|
760
|
+
val_loss=val_loss,
|
|
761
|
+
total_time=total_time_init + time.time() - start_trn_time,
|
|
762
|
+
learn_rate=current_lr,
|
|
763
|
+
dp_eps=dp_epsilon,
|
|
764
|
+
dp_delta=dp_delta,
|
|
765
|
+
)
|
|
766
|
+
# check for early stopping
|
|
767
|
+
do_stop = early_stopper(val_loss=val_loss) or has_exceeded_dp_max_epsilon
|
|
768
|
+
# scheduling for ReduceLROnPlateau
|
|
769
|
+
if isinstance(lr_scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau):
|
|
770
|
+
lr_scheduler.step(metrics=val_loss)
|
|
771
|
+
|
|
772
|
+
# log progress, either by time or by steps, whatever is shorter
|
|
773
|
+
elapsed_training_time = time.time() - start_trn_time
|
|
774
|
+
estimated_time_for_max_epochs = (max_epochs * trn_steps) * (elapsed_training_time / steps)
|
|
775
|
+
if max_training_time < estimated_time_for_max_epochs:
|
|
776
|
+
# use seconds for measuring progress against max_training_time
|
|
777
|
+
progress_total_count = max_training_time
|
|
778
|
+
progress_processed = elapsed_training_time
|
|
779
|
+
else:
|
|
780
|
+
# use steps for measuring progress against max_epochs
|
|
781
|
+
progress_total_count = max_epochs * trn_steps
|
|
782
|
+
progress_processed = steps
|
|
783
|
+
# send a progress message at least every X minutes
|
|
784
|
+
last_msg_interval = 5 * 60
|
|
785
|
+
last_msg_elapsed = time.time() - last_msg_time
|
|
786
|
+
if progress_message is None and (last_msg_elapsed > last_msg_interval or steps == 1):
|
|
787
|
+
# running mean loss of the most recent training samples
|
|
788
|
+
running_trn_loss = _calculate_average_trn_loss(trn_sample_losses, n=val_steps * val_batch_size)
|
|
789
|
+
dp_epsilon = privacy_engine.get_epsilon(dp_delta) if with_dp else None
|
|
790
|
+
progress_message = ProgressMessage(
|
|
791
|
+
epoch=epoch,
|
|
792
|
+
is_checkpoint=is_checkpoint,
|
|
793
|
+
steps=steps,
|
|
794
|
+
samples=samples,
|
|
795
|
+
trn_loss=running_trn_loss,
|
|
796
|
+
val_loss=None,
|
|
797
|
+
total_time=total_time_init + time.time() - start_trn_time,
|
|
798
|
+
learn_rate=current_lr,
|
|
799
|
+
dp_eps=dp_epsilon,
|
|
800
|
+
dp_delta=dp_delta,
|
|
801
|
+
)
|
|
802
|
+
if progress_message:
|
|
803
|
+
last_msg_time = time.time()
|
|
804
|
+
# send progress update
|
|
805
|
+
res = progress.update(
|
|
806
|
+
completed=int(progress_processed),
|
|
807
|
+
total=int(progress_total_count),
|
|
808
|
+
message=progress_message,
|
|
809
|
+
)
|
|
810
|
+
if do_validation:
|
|
811
|
+
upload_model_data_callback()
|
|
812
|
+
progress_message = None
|
|
813
|
+
if (res or {}).get("stopExecution", False):
|
|
814
|
+
_LOG.info("received STOP EXECUTION signal")
|
|
815
|
+
do_stop = True
|
|
816
|
+
|
|
817
|
+
if on_epoch_end:
|
|
818
|
+
trn_sample_losses = []
|
|
819
|
+
|
|
820
|
+
# check for max_epochs
|
|
821
|
+
if epoch > max_epochs:
|
|
822
|
+
do_stop = True
|
|
823
|
+
|
|
824
|
+
# check for max_training_time
|
|
825
|
+
if (total_time_init + time.time() - start_trn_time) > max_training_time:
|
|
826
|
+
do_stop = True
|
|
827
|
+
|
|
828
|
+
# no checkpoint is saved yet because the training stopped before the first epoch ended
|
|
829
|
+
if not model_checkpoint.has_saved_once():
|
|
830
|
+
_LOG.info("saving model weights, as none were saved so far")
|
|
831
|
+
model_checkpoint.save_checkpoint(
|
|
832
|
+
model=argn,
|
|
833
|
+
optimizer=optimizer,
|
|
834
|
+
lr_scheduler=lr_scheduler,
|
|
835
|
+
dp_accountant=privacy_engine.accountant if with_dp else None,
|
|
836
|
+
)
|
|
837
|
+
val_loss = _calculate_val_loss(model=argn, val_dataloader=val_dataloader)
|
|
838
|
+
dp_epsilon = privacy_engine.get_epsilon(dp_delta) if with_dp else None
|
|
839
|
+
# send a final message to inform how far we've progressed
|
|
840
|
+
trn_loss = _calculate_average_trn_loss(trn_sample_losses)
|
|
841
|
+
progress_message = ProgressMessage(
|
|
842
|
+
epoch=epoch,
|
|
843
|
+
is_checkpoint=1,
|
|
844
|
+
steps=steps,
|
|
845
|
+
samples=samples,
|
|
846
|
+
trn_loss=trn_loss,
|
|
847
|
+
val_loss=val_loss,
|
|
848
|
+
total_time=total_time_init + time.time() - start_trn_time,
|
|
849
|
+
learn_rate=current_lr,
|
|
850
|
+
dp_eps=dp_epsilon,
|
|
851
|
+
dp_delta=dp_delta,
|
|
852
|
+
)
|
|
853
|
+
progress.update(completed=steps, total=steps, message=progress_message)
|
|
854
|
+
# ensure everything gets uploaded
|
|
855
|
+
upload_model_data_callback()
|
|
856
|
+
_LOG.info(f"TRAIN_TABULAR finished in {time.time() - t0:.2f}s")
|