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,1157 @@
|
|
|
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 time
|
|
17
|
+
import uuid
|
|
18
|
+
from functools import partial
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
import torch
|
|
25
|
+
|
|
26
|
+
from mostlyai.engine._memory import get_available_ram_for_heuristics, get_available_vram_for_heuristics
|
|
27
|
+
from mostlyai.engine.domain import (
|
|
28
|
+
ModelEncodingType,
|
|
29
|
+
RareCategoryReplacementMethod,
|
|
30
|
+
RebalancingConfig,
|
|
31
|
+
ImputationConfig,
|
|
32
|
+
FairnessConfig,
|
|
33
|
+
)
|
|
34
|
+
from mostlyai.engine._tabular.common import load_model_weights
|
|
35
|
+
|
|
36
|
+
from mostlyai.engine._tabular.fairness import get_fairness_transforms, FairnessTransforms
|
|
37
|
+
|
|
38
|
+
from mostlyai.engine._tabular.argn import (
|
|
39
|
+
FlatModel,
|
|
40
|
+
ModelSize,
|
|
41
|
+
SequentialModel,
|
|
42
|
+
get_no_of_model_parameters,
|
|
43
|
+
)
|
|
44
|
+
from mostlyai.engine._common import (
|
|
45
|
+
ARGN_COLUMN,
|
|
46
|
+
ARGN_PROCESSOR,
|
|
47
|
+
ARGN_TABLE,
|
|
48
|
+
CTXFLT,
|
|
49
|
+
CTXSEQ,
|
|
50
|
+
SIDX_SUB_COLUMN_PREFIX,
|
|
51
|
+
SLEN_SIDX_SDEC_COLUMN,
|
|
52
|
+
SLEN_SUB_COLUMN_PREFIX,
|
|
53
|
+
apply_encoding_type_dtypes,
|
|
54
|
+
decode_slen_sidx_sdec,
|
|
55
|
+
encode_slen_sidx_sdec,
|
|
56
|
+
get_argn_name,
|
|
57
|
+
get_cardinalities,
|
|
58
|
+
get_columns_from_cardinalities,
|
|
59
|
+
get_ctx_sequence_length,
|
|
60
|
+
get_sequence_length_stats,
|
|
61
|
+
get_sub_columns_from_cardinalities,
|
|
62
|
+
is_sequential,
|
|
63
|
+
trim_sequences,
|
|
64
|
+
get_sub_columns_nested_from_cardinalities,
|
|
65
|
+
persist_data_part,
|
|
66
|
+
FixedSizeSampleBuffer,
|
|
67
|
+
SDEC_SUB_COLUMN_PREFIX,
|
|
68
|
+
ProgressCallback,
|
|
69
|
+
ProgressCallbackWrapper,
|
|
70
|
+
)
|
|
71
|
+
from mostlyai.engine._tabular.encoding import encode_df, pad_horizontally
|
|
72
|
+
from mostlyai.engine._encoding_types.tabular.categorical import (
|
|
73
|
+
CATEGORICAL_NULL_TOKEN,
|
|
74
|
+
CATEGORICAL_SUB_COL_SUFFIX,
|
|
75
|
+
CATEGORICAL_UNKNOWN_TOKEN,
|
|
76
|
+
decode_categorical,
|
|
77
|
+
)
|
|
78
|
+
from mostlyai.engine._encoding_types.tabular.character import decode_character
|
|
79
|
+
from mostlyai.engine._encoding_types.tabular.datetime import decode_datetime
|
|
80
|
+
from mostlyai.engine._encoding_types.tabular.itt import decode_itt
|
|
81
|
+
from mostlyai.engine._encoding_types.tabular.lat_long import decode_latlong
|
|
82
|
+
from mostlyai.engine._encoding_types.tabular.numeric import (
|
|
83
|
+
NUMERIC_BINNED_NULL_TOKEN,
|
|
84
|
+
NUMERIC_BINNED_SUB_COL_SUFFIX,
|
|
85
|
+
NUMERIC_BINNED_UNKNOWN_TOKEN,
|
|
86
|
+
NUMERIC_DISCRETE_NULL_TOKEN,
|
|
87
|
+
NUMERIC_DISCRETE_SUB_COL_SUFFIX,
|
|
88
|
+
NUMERIC_DISCRETE_UNKNOWN_TOKEN,
|
|
89
|
+
decode_numeric,
|
|
90
|
+
)
|
|
91
|
+
from mostlyai.engine._workspace import Workspace, ensure_workspace_dir, reset_dir
|
|
92
|
+
|
|
93
|
+
_LOG = logging.getLogger(__name__)
|
|
94
|
+
|
|
95
|
+
DUMMY_CONTEXT_KEY = "__dummy_context_key"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
CodeProbabilities = dict[int, float] # CategoryProbabilities after encoding, e.g. {0: 0.3, 1: 0.4}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _resolve_gen_column_order(
|
|
102
|
+
column_stats: dict,
|
|
103
|
+
cardinalities: dict,
|
|
104
|
+
rebalancing: RebalancingConfig | None = None,
|
|
105
|
+
imputation: ImputationConfig | None = None,
|
|
106
|
+
sample_seed: pd.DataFrame | None = None,
|
|
107
|
+
fairness: FairnessConfig | None = None,
|
|
108
|
+
):
|
|
109
|
+
column_order = get_columns_from_cardinalities(cardinalities)
|
|
110
|
+
|
|
111
|
+
# Reorder columns in the following order:
|
|
112
|
+
# 0. SLEN/SIDX column
|
|
113
|
+
# 1. Sample seed columns
|
|
114
|
+
# 2. Rebalancing column
|
|
115
|
+
# 3. Fairness sensitive columns (which are not imputation columns)
|
|
116
|
+
# 4. Fairness sensitive columns (which are imputation columns as well)
|
|
117
|
+
# 5. The rest of the columns
|
|
118
|
+
# 6. Imputation columns (which are not fairness sensitive columns)
|
|
119
|
+
# 7. Fairness target column
|
|
120
|
+
|
|
121
|
+
if imputation:
|
|
122
|
+
# imputed columns should be at the end in the generation model
|
|
123
|
+
imputation_argn = [
|
|
124
|
+
get_argn_name(
|
|
125
|
+
argn_processor=column_stats[col][ARGN_PROCESSOR],
|
|
126
|
+
argn_table=column_stats[col][ARGN_TABLE],
|
|
127
|
+
argn_column=column_stats[col][ARGN_COLUMN],
|
|
128
|
+
)
|
|
129
|
+
for col in imputation.columns
|
|
130
|
+
if col in column_stats
|
|
131
|
+
]
|
|
132
|
+
column_order = [c for c in column_order if c not in imputation_argn] + imputation_argn
|
|
133
|
+
else:
|
|
134
|
+
imputation_argn = []
|
|
135
|
+
|
|
136
|
+
if fairness:
|
|
137
|
+
# bring sensitive columns to the front and target column to the back
|
|
138
|
+
sensitive_columns_argn = [
|
|
139
|
+
get_argn_name(
|
|
140
|
+
argn_processor=column_stats[col][ARGN_PROCESSOR],
|
|
141
|
+
argn_table=column_stats[col][ARGN_TABLE],
|
|
142
|
+
argn_column=column_stats[col][ARGN_COLUMN],
|
|
143
|
+
)
|
|
144
|
+
for col in fairness.sensitive_columns
|
|
145
|
+
if col in column_stats
|
|
146
|
+
]
|
|
147
|
+
# imputed sensitive columns should be after other usual sensitive columns
|
|
148
|
+
sensitive_columns_argn = [c for c in sensitive_columns_argn if c not in imputation_argn] + [
|
|
149
|
+
c for c in sensitive_columns_argn if c in imputation_argn
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
target_column_argn = get_argn_name(
|
|
153
|
+
argn_processor=column_stats[fairness.target_column][ARGN_PROCESSOR],
|
|
154
|
+
argn_table=column_stats[fairness.target_column][ARGN_TABLE],
|
|
155
|
+
argn_column=column_stats[fairness.target_column][ARGN_COLUMN],
|
|
156
|
+
)
|
|
157
|
+
column_order = (
|
|
158
|
+
sensitive_columns_argn
|
|
159
|
+
+ [c for c in column_order if c not in sensitive_columns_argn + [target_column_argn]]
|
|
160
|
+
+ [target_column_argn]
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if rebalancing:
|
|
164
|
+
# rebalance column should be at the beginning in the generation model
|
|
165
|
+
# rebalancing has higher priority than imputation
|
|
166
|
+
if rebalancing.column in column_stats:
|
|
167
|
+
rebalance_column_argn = get_argn_name(
|
|
168
|
+
argn_processor=column_stats[rebalancing.column][ARGN_PROCESSOR],
|
|
169
|
+
argn_table=column_stats[rebalancing.column][ARGN_TABLE],
|
|
170
|
+
argn_column=column_stats[rebalancing.column][ARGN_COLUMN],
|
|
171
|
+
)
|
|
172
|
+
column_order = [rebalance_column_argn] + [c for c in column_order if c != rebalance_column_argn]
|
|
173
|
+
|
|
174
|
+
if sample_seed is not None:
|
|
175
|
+
# sample_seed columns should be at the beginning in the generation model
|
|
176
|
+
# sample_seed has higher priority than rebalancing and imputation
|
|
177
|
+
seed_columns_argn = [
|
|
178
|
+
get_argn_name(
|
|
179
|
+
argn_processor=column_stats[col][ARGN_PROCESSOR],
|
|
180
|
+
argn_table=column_stats[col][ARGN_TABLE],
|
|
181
|
+
argn_column=column_stats[col][ARGN_COLUMN],
|
|
182
|
+
)
|
|
183
|
+
for col in sample_seed.columns
|
|
184
|
+
if col in column_stats
|
|
185
|
+
]
|
|
186
|
+
column_order = seed_columns_argn + [c for c in column_order if c not in seed_columns_argn]
|
|
187
|
+
|
|
188
|
+
if SLEN_SIDX_SDEC_COLUMN in column_order:
|
|
189
|
+
# SLEN/SIDX column needs to be the first one in the generation model
|
|
190
|
+
column_order = [SLEN_SIDX_SDEC_COLUMN] + [c for c in column_order if c != SLEN_SIDX_SDEC_COLUMN]
|
|
191
|
+
|
|
192
|
+
return column_order
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _generate_primary_keys(size: int, type: Literal["uuid", "int"] = "uuid") -> pd.Series:
|
|
196
|
+
if type == "uuid":
|
|
197
|
+
# generate watermarked 36-chars UUIDv4s
|
|
198
|
+
# e.g. mostly2b-d87c-4825-884f-611b309c3c55
|
|
199
|
+
return pd.Series([f"mostly{str(uuid.uuid4())[6:]}" for _ in range(size)], dtype="string")
|
|
200
|
+
else:
|
|
201
|
+
return pd.Series(range(size), dtype="int")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _batch_df(df: pd.DataFrame, no_of_batches: int) -> pd.DataFrame:
|
|
205
|
+
rows_per_batch = len(df) / no_of_batches
|
|
206
|
+
running_total = pd.Series(range(len(df))) / rows_per_batch
|
|
207
|
+
df = df.assign(__BATCH=running_total.astype(int) + 1)
|
|
208
|
+
return df
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _pad_vertically(df: pd.DataFrame, batch_size: int, primary_key: str) -> pd.DataFrame:
|
|
212
|
+
"""
|
|
213
|
+
Append rows with zeros so that `df` has `batch_size` rows.
|
|
214
|
+
"""
|
|
215
|
+
# determine number of required padded rows
|
|
216
|
+
no_of_pad_rows = batch_size - df.shape[0]
|
|
217
|
+
if no_of_pad_rows <= 0:
|
|
218
|
+
return df
|
|
219
|
+
|
|
220
|
+
# create padded rows with zeros
|
|
221
|
+
def pad_flat(c):
|
|
222
|
+
return pd.Series(np.repeat([0], no_of_pad_rows), name=c)
|
|
223
|
+
|
|
224
|
+
def pad_seq(c):
|
|
225
|
+
return pd.Series(np.empty((no_of_pad_rows, 0)).tolist(), name=c)
|
|
226
|
+
|
|
227
|
+
pads = pd.concat(
|
|
228
|
+
[pad_seq(c) if is_sequential(df[c]) else pad_flat(c) for c in df.columns],
|
|
229
|
+
axis=1,
|
|
230
|
+
)
|
|
231
|
+
# flag padded rows by setting its key column to None
|
|
232
|
+
pads[primary_key] = None
|
|
233
|
+
# concatenate the padded rows to original data
|
|
234
|
+
df = pd.concat([df, pads], axis=0)
|
|
235
|
+
df.reset_index(drop=True, inplace=True)
|
|
236
|
+
return df
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _reshape_pt_to_pandas(
|
|
240
|
+
data: list[torch.Tensor], sub_cols: list[str], keys: list[pd.Series], key_name: str
|
|
241
|
+
) -> pd.DataFrame:
|
|
242
|
+
# len(data)=seq_len, data[0].shape=(1<=x<=batch_size, n_sub_cols, 1)
|
|
243
|
+
# len(keys)=seq_len, keys[0].shape=(1<=x<=batch_size,)
|
|
244
|
+
# len(sub_cols)=n_sub_cols
|
|
245
|
+
assert len(data) == len(keys)
|
|
246
|
+
for step_data, step_keys in zip(data, keys):
|
|
247
|
+
assert step_data.shape[0] == step_keys.shape[0]
|
|
248
|
+
seq_len = len(data)
|
|
249
|
+
if seq_len == 0:
|
|
250
|
+
return pd.DataFrame(columns=[key_name] + sub_cols)
|
|
251
|
+
# transform from list[torch.Tensor] to pd.DataFrame, by concatenating sequence steps
|
|
252
|
+
# df.shape=(sum(1<=x<=batch_size), n_sub_cols)
|
|
253
|
+
df = pd.concat(
|
|
254
|
+
[
|
|
255
|
+
pd.DataFrame(
|
|
256
|
+
step_tensor.squeeze(-1).detach().cpu().numpy(),
|
|
257
|
+
columns=sub_cols,
|
|
258
|
+
dtype="int32",
|
|
259
|
+
)
|
|
260
|
+
for step_tensor in data
|
|
261
|
+
],
|
|
262
|
+
axis=0,
|
|
263
|
+
).reset_index(drop=True)
|
|
264
|
+
# transform keys from list[pd.Series] to pd.Series, by concatenating sequence steps
|
|
265
|
+
# keys.shape=(sum(1<=x<=batch_size),)
|
|
266
|
+
keys = pd.concat(keys, axis=0).rename(key_name).reset_index(drop=True)
|
|
267
|
+
return pd.concat([keys, df], axis=1)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _continue_sequence_mask(
|
|
271
|
+
step_output: torch.Tensor,
|
|
272
|
+
sub_cols: list[str],
|
|
273
|
+
step_keys: pd.Series,
|
|
274
|
+
key_name: str,
|
|
275
|
+
seq_len_min: int,
|
|
276
|
+
seq_len_max: int,
|
|
277
|
+
):
|
|
278
|
+
# reshape tensor to pandas
|
|
279
|
+
syn = _reshape_pt_to_pandas(
|
|
280
|
+
data=[step_output],
|
|
281
|
+
sub_cols=sub_cols,
|
|
282
|
+
keys=[step_keys],
|
|
283
|
+
key_name=key_name,
|
|
284
|
+
)
|
|
285
|
+
# decode SLEN/SIDX columns
|
|
286
|
+
syn[SIDX_SUB_COLUMN_PREFIX] = decode_slen_sidx_sdec(syn, seq_len_max, prefix=SIDX_SUB_COLUMN_PREFIX)
|
|
287
|
+
syn[SLEN_SUB_COLUMN_PREFIX] = decode_slen_sidx_sdec(syn, seq_len_max, prefix=SLEN_SUB_COLUMN_PREFIX)
|
|
288
|
+
syn[SLEN_SUB_COLUMN_PREFIX] = np.maximum(seq_len_min, syn[SLEN_SUB_COLUMN_PREFIX])
|
|
289
|
+
# calculate stop sequence mask (True=continue, False=stop)
|
|
290
|
+
return syn[SIDX_SUB_COLUMN_PREFIX] < syn[SLEN_SUB_COLUMN_PREFIX]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _post_process_decoding(
|
|
294
|
+
syn: pd.DataFrame,
|
|
295
|
+
tgt_primary_key: str | None = None,
|
|
296
|
+
) -> pd.DataFrame:
|
|
297
|
+
# remove dummy context key (if exists)
|
|
298
|
+
if DUMMY_CONTEXT_KEY in syn:
|
|
299
|
+
syn = syn.drop(columns=DUMMY_CONTEXT_KEY)
|
|
300
|
+
|
|
301
|
+
# generate primary keys, if they are not present
|
|
302
|
+
if tgt_primary_key and tgt_primary_key not in syn:
|
|
303
|
+
syn[tgt_primary_key] = _generate_primary_keys(len(syn), type="uuid")
|
|
304
|
+
|
|
305
|
+
return syn
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
##################
|
|
309
|
+
### HEURISTICS ###
|
|
310
|
+
##################
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _generation_batch_size_heuristic(mem_available_gb: float, ctx_stats: dict, tgt_stats: dict, device: torch.device):
|
|
314
|
+
tgt_cardinalities = get_cardinalities(tgt_stats)
|
|
315
|
+
ctx_cardinalities = get_cardinalities(ctx_stats)
|
|
316
|
+
ctxflt_cardinalities = {k: v for k, v in ctx_cardinalities.items() if k.startswith(CTXFLT)}
|
|
317
|
+
ctxseq_cardinalities = {k: v for k, v in ctx_cardinalities.items() if k.startswith(CTXSEQ)}
|
|
318
|
+
ctxseq_max_lengths = get_ctx_sequence_length(ctx_stats, key="max")
|
|
319
|
+
ctxseq_table_sub_columns = get_sub_columns_nested_from_cardinalities(ctxseq_cardinalities, groupby="tables")
|
|
320
|
+
|
|
321
|
+
one_hot_unit_bytes = 4
|
|
322
|
+
mem_available_bytes = mem_available_gb * 1_000 * 1_000 * 1_000
|
|
323
|
+
|
|
324
|
+
ctxflt_one_hot = sum(ctxflt_cardinalities.values())
|
|
325
|
+
ctxflt_one_hot_bytes = ctxflt_one_hot * one_hot_unit_bytes
|
|
326
|
+
|
|
327
|
+
ctxseq_one_hots = []
|
|
328
|
+
for table, sub_columns in ctxseq_table_sub_columns.items():
|
|
329
|
+
one_hot = sum([ctxseq_cardinalities[sub_column] for sub_column in sub_columns]) * ctxseq_max_lengths[table]
|
|
330
|
+
ctxseq_one_hots.append(one_hot)
|
|
331
|
+
ctxseq_one_hot = sum(ctxseq_one_hots)
|
|
332
|
+
ctxseq_one_hot_bytes = ctxseq_one_hot * one_hot_unit_bytes
|
|
333
|
+
|
|
334
|
+
tgt_one_hot = sum(tgt_cardinalities.values())
|
|
335
|
+
tgt_one_hot_bytes = tgt_one_hot * one_hot_unit_bytes
|
|
336
|
+
|
|
337
|
+
sample_bytes = ctxflt_one_hot_bytes + ctxseq_one_hot_bytes + tgt_one_hot_bytes
|
|
338
|
+
sample_kb = sample_bytes / 1_000
|
|
339
|
+
scaling_factor = 0.1
|
|
340
|
+
|
|
341
|
+
batch_size = int((mem_available_bytes // max(1, sample_bytes)) * scaling_factor)
|
|
342
|
+
device_max = 10_000 if device.type == "cuda" else 100_000
|
|
343
|
+
batch_size = int(np.clip(batch_size, a_min=100, a_max=device_max))
|
|
344
|
+
|
|
345
|
+
_LOG.info(
|
|
346
|
+
f"batch_size heuristic: {batch_size:,} ({mem_available_gb=:.1f}GB, {sample_kb=:.1f}KB, {scaling_factor=:.1f})"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return batch_size
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
#########################
|
|
353
|
+
### PROGRAMMABLE DATA ###
|
|
354
|
+
#########################
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _fix_rare_token_probs(
|
|
358
|
+
stats: dict,
|
|
359
|
+
rare_category_replacement_method: RareCategoryReplacementMethod | None = None,
|
|
360
|
+
) -> dict[str, dict[str, CodeProbabilities]]:
|
|
361
|
+
# suppress rare token for categorical when no_of_rare_categories == 0
|
|
362
|
+
mask = {
|
|
363
|
+
col: {CATEGORICAL_SUB_COL_SUFFIX: {col_stats["codes"][CATEGORICAL_UNKNOWN_TOKEN]: 0.0}}
|
|
364
|
+
for col, col_stats in stats["columns"].items()
|
|
365
|
+
if col_stats["encoding_type"] == ModelEncodingType.tabular_categorical
|
|
366
|
+
if "codes" in col_stats
|
|
367
|
+
if col_stats.get("no_of_rare_categories", 0) == 0
|
|
368
|
+
}
|
|
369
|
+
# suppress rare token for categorical if RareCategoryReplacementMethod is sample
|
|
370
|
+
if rare_category_replacement_method == RareCategoryReplacementMethod.sample:
|
|
371
|
+
mask |= {
|
|
372
|
+
col: {CATEGORICAL_SUB_COL_SUFFIX: {col_stats["codes"][CATEGORICAL_UNKNOWN_TOKEN]: 0.0}}
|
|
373
|
+
for col, col_stats in stats["columns"].items()
|
|
374
|
+
if col_stats["encoding_type"] == ModelEncodingType.tabular_categorical
|
|
375
|
+
if "codes" in col_stats
|
|
376
|
+
}
|
|
377
|
+
# always suppress rare token for numeric_binned
|
|
378
|
+
mask |= {
|
|
379
|
+
col: {NUMERIC_BINNED_SUB_COL_SUFFIX: {col_stats["codes"][NUMERIC_BINNED_UNKNOWN_TOKEN]: 0.0}}
|
|
380
|
+
for col, col_stats in stats["columns"].items()
|
|
381
|
+
if col_stats["encoding_type"] == ModelEncodingType.tabular_numeric_binned
|
|
382
|
+
if "codes" in col_stats
|
|
383
|
+
}
|
|
384
|
+
# always suppress rare token for numeric_discrete
|
|
385
|
+
mask |= {
|
|
386
|
+
col: {NUMERIC_DISCRETE_SUB_COL_SUFFIX: {col_stats["codes"][NUMERIC_DISCRETE_UNKNOWN_TOKEN]: 0.0}}
|
|
387
|
+
for col, col_stats in stats["columns"].items()
|
|
388
|
+
if col_stats["encoding_type"] == ModelEncodingType.tabular_numeric_discrete
|
|
389
|
+
if "codes" in col_stats
|
|
390
|
+
}
|
|
391
|
+
return mask
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _fix_imputation_probs(
|
|
395
|
+
stats: dict,
|
|
396
|
+
imputation: ImputationConfig | None = None,
|
|
397
|
+
) -> dict[str, dict[str, CodeProbabilities]]:
|
|
398
|
+
imputation = imputation.columns if imputation is not None else []
|
|
399
|
+
_LOG.info(f"imputation: {imputation}")
|
|
400
|
+
fixed_probs: dict[str, dict[str, CodeProbabilities]] = {}
|
|
401
|
+
for col in imputation:
|
|
402
|
+
if col not in stats["columns"]:
|
|
403
|
+
_LOG.info(f"imputed [{col}] not found in stats")
|
|
404
|
+
continue
|
|
405
|
+
col_stats = stats["columns"][col]
|
|
406
|
+
encoding_type = col_stats["encoding_type"]
|
|
407
|
+
# null_name will be either None, "na" or "nan"
|
|
408
|
+
null_subcol = next(iter([k[4:] for k in col_stats.keys() if k in ["has_na", "has_nan"]]), None)
|
|
409
|
+
if null_subcol is not None and col_stats[f"has_{null_subcol}"]:
|
|
410
|
+
# column has separate null sub column and there are some nulls
|
|
411
|
+
code_null = 1
|
|
412
|
+
col_fixed_probs = {col: {null_subcol: {code_null: 0.0}}}
|
|
413
|
+
elif encoding_type in [
|
|
414
|
+
ModelEncodingType.tabular_categorical,
|
|
415
|
+
ModelEncodingType.tabular_numeric_discrete,
|
|
416
|
+
ModelEncodingType.tabular_numeric_binned,
|
|
417
|
+
]:
|
|
418
|
+
# column is categorical-like and has single sub column
|
|
419
|
+
sub_column = {
|
|
420
|
+
ModelEncodingType.tabular_categorical: CATEGORICAL_SUB_COL_SUFFIX,
|
|
421
|
+
ModelEncodingType.tabular_numeric_discrete: NUMERIC_DISCRETE_SUB_COL_SUFFIX,
|
|
422
|
+
ModelEncodingType.tabular_numeric_binned: NUMERIC_BINNED_SUB_COL_SUFFIX,
|
|
423
|
+
}[encoding_type]
|
|
424
|
+
code_probs = {
|
|
425
|
+
ModelEncodingType.tabular_categorical: {
|
|
426
|
+
CATEGORICAL_NULL_TOKEN: 0.0,
|
|
427
|
+
CATEGORICAL_UNKNOWN_TOKEN: 0.0,
|
|
428
|
+
},
|
|
429
|
+
ModelEncodingType.tabular_numeric_discrete: {NUMERIC_DISCRETE_NULL_TOKEN: 0.0},
|
|
430
|
+
ModelEncodingType.tabular_numeric_binned: {NUMERIC_BINNED_NULL_TOKEN: 0.0},
|
|
431
|
+
}[encoding_type]
|
|
432
|
+
# map and filter out codes that did not occur (happens when there are no nulls)
|
|
433
|
+
col_fixed_probs = {
|
|
434
|
+
col: {
|
|
435
|
+
sub_column: {
|
|
436
|
+
col_stats["codes"][category]: probs
|
|
437
|
+
for category, probs in code_probs.items()
|
|
438
|
+
if category in col_stats["codes"]
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
else:
|
|
443
|
+
col_fixed_probs = {}
|
|
444
|
+
fixed_probs |= col_fixed_probs
|
|
445
|
+
return fixed_probs
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _fix_rebalancing_probs(
|
|
449
|
+
stats: dict,
|
|
450
|
+
rebalancing: RebalancingConfig | None = None,
|
|
451
|
+
) -> dict[str, dict[str, CodeProbabilities]]:
|
|
452
|
+
column, probabilities = (rebalancing.column, rebalancing.probabilities) if rebalancing else (None, {})
|
|
453
|
+
_LOG.info(f"rebalance_column: {column}")
|
|
454
|
+
_LOG.info(f"rebalance_probabilities: {probabilities}")
|
|
455
|
+
mask = {}
|
|
456
|
+
if (
|
|
457
|
+
column is not None
|
|
458
|
+
and column in stats["columns"]
|
|
459
|
+
and stats["columns"][column]["encoding_type"] == ModelEncodingType.tabular_categorical
|
|
460
|
+
and "codes" in stats["columns"][column]
|
|
461
|
+
):
|
|
462
|
+
col_codes = stats["columns"][column]["codes"]
|
|
463
|
+
code_probabilities = {
|
|
464
|
+
col_codes[category]: max(0.0, prob) for category, prob in probabilities.items() if category in col_codes
|
|
465
|
+
}
|
|
466
|
+
# normalize probabilities if they sum up to more than 1.0
|
|
467
|
+
total_share = sum(code_probabilities.values())
|
|
468
|
+
if total_share > 1.0:
|
|
469
|
+
code_probabilities = {code: share / total_share for code, share in code_probabilities.items()}
|
|
470
|
+
if code_probabilities:
|
|
471
|
+
mask = {column: {CATEGORICAL_SUB_COL_SUFFIX: code_probabilities}}
|
|
472
|
+
|
|
473
|
+
return mask
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _translate_fixed_probs(
|
|
477
|
+
fixed_probs: dict[str, dict[str, CodeProbabilities]], stats: dict
|
|
478
|
+
) -> dict[str, CodeProbabilities]:
|
|
479
|
+
# translate fixed probs to ARGN conventions
|
|
480
|
+
mask = {
|
|
481
|
+
get_argn_name(
|
|
482
|
+
argn_processor=stats["columns"][col][ARGN_PROCESSOR],
|
|
483
|
+
argn_table=stats["columns"][col][ARGN_TABLE],
|
|
484
|
+
argn_column=stats["columns"][col][ARGN_COLUMN],
|
|
485
|
+
argn_sub_column=sub_col,
|
|
486
|
+
): sub_col_mask
|
|
487
|
+
for col, col_mask in fixed_probs.items()
|
|
488
|
+
for sub_col, sub_col_mask in col_mask.items()
|
|
489
|
+
}
|
|
490
|
+
return mask
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _deepmerge(*dictionaries: dict, merged: dict | None = None) -> dict:
|
|
494
|
+
merged = merged or {}
|
|
495
|
+
for dictionary in dictionaries:
|
|
496
|
+
for key, value in dictionary.items():
|
|
497
|
+
if isinstance(value, dict):
|
|
498
|
+
if key not in merged:
|
|
499
|
+
merged[key] = {}
|
|
500
|
+
merged[key] |= _deepmerge(value, merged[key])
|
|
501
|
+
else:
|
|
502
|
+
merged[key] = value
|
|
503
|
+
return merged
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
##################
|
|
507
|
+
### DECODE ###
|
|
508
|
+
##################
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _decode_df(
|
|
512
|
+
df_encoded: pd.DataFrame,
|
|
513
|
+
stats: dict,
|
|
514
|
+
context_key: str | None = None,
|
|
515
|
+
prev_steps: dict | None = None,
|
|
516
|
+
) -> pd.DataFrame:
|
|
517
|
+
columns = []
|
|
518
|
+
if context_key and context_key in df_encoded.columns:
|
|
519
|
+
columns.append(df_encoded[context_key])
|
|
520
|
+
for column, column_stats in stats["columns"].items():
|
|
521
|
+
if column_stats.keys() == {"encoding_type"}:
|
|
522
|
+
# training data was empty
|
|
523
|
+
values = pd.Series(data=[], name=column, dtype="object")
|
|
524
|
+
columns.append(values)
|
|
525
|
+
continue
|
|
526
|
+
sub_columns = [
|
|
527
|
+
get_argn_name(
|
|
528
|
+
argn_processor=column_stats[ARGN_PROCESSOR],
|
|
529
|
+
argn_table=column_stats[ARGN_TABLE],
|
|
530
|
+
argn_column=column_stats[ARGN_COLUMN],
|
|
531
|
+
argn_sub_column=sub_col,
|
|
532
|
+
)
|
|
533
|
+
for sub_col in column_stats["cardinalities"].keys()
|
|
534
|
+
]
|
|
535
|
+
# fetch column-specific sub_columns from data
|
|
536
|
+
df_encoded_col = df_encoded[sub_columns]
|
|
537
|
+
# remove column prefixes before decoding
|
|
538
|
+
df_encoded_col.columns = [
|
|
539
|
+
ocol.replace(
|
|
540
|
+
# replace conventional column name without sub_column part
|
|
541
|
+
get_argn_name(
|
|
542
|
+
argn_processor=column_stats[ARGN_PROCESSOR],
|
|
543
|
+
argn_table=column_stats[ARGN_TABLE],
|
|
544
|
+
argn_column=column_stats[ARGN_COLUMN],
|
|
545
|
+
argn_sub_column="",
|
|
546
|
+
),
|
|
547
|
+
"",
|
|
548
|
+
)
|
|
549
|
+
for ocol in df_encoded_col.columns
|
|
550
|
+
]
|
|
551
|
+
# handle column prev_steps
|
|
552
|
+
prev_steps_col = None
|
|
553
|
+
if prev_steps is not None:
|
|
554
|
+
prev_steps[column] = prev_steps.get(column, {})
|
|
555
|
+
prev_steps_col = prev_steps[column]
|
|
556
|
+
# decode encoded sub_columns into single decoded column
|
|
557
|
+
values = _decode_col(
|
|
558
|
+
df_encoded=df_encoded_col,
|
|
559
|
+
stats=column_stats,
|
|
560
|
+
context_keys=df_encoded[context_key] if context_key in df_encoded.columns else None,
|
|
561
|
+
prev_steps=prev_steps_col,
|
|
562
|
+
)
|
|
563
|
+
values.name = column
|
|
564
|
+
columns.append(values)
|
|
565
|
+
return pd.concat(columns, axis=1)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _decode_col(
|
|
569
|
+
df_encoded: pd.DataFrame,
|
|
570
|
+
stats: dict,
|
|
571
|
+
context_keys: pd.Series | None = None,
|
|
572
|
+
prev_steps: dict | None = None,
|
|
573
|
+
) -> pd.Series:
|
|
574
|
+
if df_encoded.empty:
|
|
575
|
+
return pd.Series()
|
|
576
|
+
|
|
577
|
+
encoding_type = stats["encoding_type"]
|
|
578
|
+
|
|
579
|
+
if encoding_type == ModelEncodingType.tabular_categorical:
|
|
580
|
+
values = decode_categorical(df_encoded=df_encoded, stats=stats)
|
|
581
|
+
elif encoding_type in [
|
|
582
|
+
ModelEncodingType.tabular_numeric_auto,
|
|
583
|
+
ModelEncodingType.tabular_numeric_discrete,
|
|
584
|
+
ModelEncodingType.tabular_numeric_binned,
|
|
585
|
+
ModelEncodingType.tabular_numeric_digit,
|
|
586
|
+
]:
|
|
587
|
+
values = decode_numeric(df_encoded=df_encoded, stats=stats)
|
|
588
|
+
elif encoding_type == ModelEncodingType.tabular_datetime:
|
|
589
|
+
values = decode_datetime(df_encoded=df_encoded, stats=stats)
|
|
590
|
+
elif encoding_type == ModelEncodingType.tabular_datetime_relative:
|
|
591
|
+
values = decode_itt(
|
|
592
|
+
df_encoded=df_encoded,
|
|
593
|
+
stats=stats,
|
|
594
|
+
context_keys=context_keys,
|
|
595
|
+
prev_steps=prev_steps,
|
|
596
|
+
)
|
|
597
|
+
elif encoding_type == ModelEncodingType.tabular_character:
|
|
598
|
+
values = decode_character(df_encoded=df_encoded, stats=stats)
|
|
599
|
+
elif encoding_type == ModelEncodingType.tabular_lat_long:
|
|
600
|
+
values = decode_latlong(df_encoded=df_encoded, stats=stats)
|
|
601
|
+
return values
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def decode_buffered_samples(
|
|
605
|
+
buffer: FixedSizeSampleBuffer,
|
|
606
|
+
tgt_stats: dict,
|
|
607
|
+
tgt_sub_columns: list[str],
|
|
608
|
+
tgt_primary_key: str,
|
|
609
|
+
tgt_context_key: str,
|
|
610
|
+
decode_prev_steps: dict | None = None,
|
|
611
|
+
) -> pd.DataFrame:
|
|
612
|
+
is_sequential = tgt_stats["is_sequential"]
|
|
613
|
+
seq_len_stats = get_sequence_length_stats(tgt_stats)
|
|
614
|
+
seq_len_min = seq_len_stats["min"]
|
|
615
|
+
seq_len_max = seq_len_stats["max"]
|
|
616
|
+
|
|
617
|
+
assert not buffer.is_empty() or seq_len_max == 0
|
|
618
|
+
|
|
619
|
+
if is_sequential:
|
|
620
|
+
data, keys = zip(*buffer.buffer) if buffer.buffer else ([], [])
|
|
621
|
+
syn = _reshape_pt_to_pandas(
|
|
622
|
+
data=data,
|
|
623
|
+
sub_cols=tgt_sub_columns,
|
|
624
|
+
keys=keys,
|
|
625
|
+
key_name=tgt_context_key,
|
|
626
|
+
)
|
|
627
|
+
# trim sequences to min and max length
|
|
628
|
+
syn = trim_sequences(
|
|
629
|
+
syn=syn,
|
|
630
|
+
tgt_context_key=tgt_context_key,
|
|
631
|
+
seq_len_min=seq_len_min,
|
|
632
|
+
seq_len_max=seq_len_max,
|
|
633
|
+
)
|
|
634
|
+
else:
|
|
635
|
+
(data,) = zip(*buffer.buffer)
|
|
636
|
+
syn = pd.concat(data, axis=0).reset_index(drop=True)
|
|
637
|
+
|
|
638
|
+
# extract pre-defined seed columns, if provided
|
|
639
|
+
seed_columns = [col for col in syn.columns if col.startswith("__seed:")]
|
|
640
|
+
if seed_columns:
|
|
641
|
+
df_seed = syn[seed_columns].rename(columns=lambda col: col.replace("__seed:", "", 1))
|
|
642
|
+
else:
|
|
643
|
+
df_seed = pd.DataFrame()
|
|
644
|
+
|
|
645
|
+
# decode generated data
|
|
646
|
+
_LOG.info(f"decode generated data {syn.shape}")
|
|
647
|
+
syn = _decode_df(
|
|
648
|
+
df_encoded=syn,
|
|
649
|
+
stats=tgt_stats,
|
|
650
|
+
context_key=tgt_context_key,
|
|
651
|
+
prev_steps=decode_prev_steps,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
# preserve all seed columns
|
|
655
|
+
for col in df_seed.columns:
|
|
656
|
+
syn[col] = df_seed[col]
|
|
657
|
+
|
|
658
|
+
# postprocess generated data
|
|
659
|
+
_LOG.info(f"post-process generated data {syn.shape}")
|
|
660
|
+
syn = _post_process_decoding(
|
|
661
|
+
syn,
|
|
662
|
+
tgt_primary_key=tgt_primary_key,
|
|
663
|
+
)
|
|
664
|
+
return syn
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
##################
|
|
668
|
+
### GENERATION ###
|
|
669
|
+
##################
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
@torch.no_grad()
|
|
673
|
+
def generate(
|
|
674
|
+
*,
|
|
675
|
+
ctx_data: pd.DataFrame | None = None,
|
|
676
|
+
seed_data: pd.DataFrame | None = None,
|
|
677
|
+
sample_size: int | None = None,
|
|
678
|
+
batch_size: int | None = None,
|
|
679
|
+
rare_category_replacement_method: RareCategoryReplacementMethod | str = RareCategoryReplacementMethod.constant,
|
|
680
|
+
sampling_temperature: float = 1.0,
|
|
681
|
+
sampling_top_p: float = 1.0,
|
|
682
|
+
rebalancing: RebalancingConfig | dict | None = None,
|
|
683
|
+
imputation: ImputationConfig | dict | None = None,
|
|
684
|
+
fairness: FairnessConfig | dict | None = None,
|
|
685
|
+
device: torch.device | str | None = None,
|
|
686
|
+
workspace_dir: str | Path = "engine-ws",
|
|
687
|
+
update_progress: ProgressCallback | None = None,
|
|
688
|
+
) -> None:
|
|
689
|
+
_LOG.info("GENERATE_TABULAR started")
|
|
690
|
+
t0 = time.time()
|
|
691
|
+
with ProgressCallbackWrapper(update_progress) as progress:
|
|
692
|
+
# build paths based on workspace dir
|
|
693
|
+
workspace_dir = ensure_workspace_dir(workspace_dir)
|
|
694
|
+
workspace = Workspace(workspace_dir)
|
|
695
|
+
output_path = workspace.generated_data_path
|
|
696
|
+
reset_dir(output_path)
|
|
697
|
+
|
|
698
|
+
model_configs = workspace.model_tabular_configs.read()
|
|
699
|
+
tgt_stats = workspace.tgt_stats.read()
|
|
700
|
+
is_sequential = tgt_stats["is_sequential"]
|
|
701
|
+
_LOG.info(f"{is_sequential=}")
|
|
702
|
+
has_context = workspace.ctx_stats.path.exists()
|
|
703
|
+
_LOG.info(f"{has_context=}")
|
|
704
|
+
ctx_stats = workspace.ctx_stats.read()
|
|
705
|
+
|
|
706
|
+
tgt_cardinalities = get_cardinalities(tgt_stats)
|
|
707
|
+
tgt_sub_columns = get_sub_columns_from_cardinalities(tgt_cardinalities)
|
|
708
|
+
ctx_cardinalities = get_cardinalities(ctx_stats)
|
|
709
|
+
ctx_sub_columns = get_sub_columns_from_cardinalities(ctx_cardinalities)
|
|
710
|
+
if is_sequential and model_configs.get("model_units"):
|
|
711
|
+
# remain backwards compatible to models trained without SDEC
|
|
712
|
+
has_sdec = any([f"{SDEC_SUB_COLUMN_PREFIX}cat" in k for k in model_configs.get("model_units").keys()])
|
|
713
|
+
if not has_sdec:
|
|
714
|
+
_LOG.warning("SDEC not found in model_units, removing SDEC columns from tgt_cardinalities")
|
|
715
|
+
del tgt_cardinalities[f"{SDEC_SUB_COLUMN_PREFIX}cat"]
|
|
716
|
+
tgt_sub_columns.remove(f"{SDEC_SUB_COLUMN_PREFIX}cat")
|
|
717
|
+
_LOG.info(f"{len(tgt_sub_columns)=}")
|
|
718
|
+
_LOG.info(f"{len(ctx_sub_columns)=}")
|
|
719
|
+
|
|
720
|
+
# read model config
|
|
721
|
+
model_units = model_configs.get("model_units") or ModelSize.M
|
|
722
|
+
_LOG.debug(f"{model_units=}")
|
|
723
|
+
enable_flexible_generation = model_configs.get("enable_flexible_generation")
|
|
724
|
+
_LOG.info(f"{enable_flexible_generation=}")
|
|
725
|
+
|
|
726
|
+
# resolve device
|
|
727
|
+
device = (
|
|
728
|
+
torch.device(device)
|
|
729
|
+
if device is not None
|
|
730
|
+
else (torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu"))
|
|
731
|
+
)
|
|
732
|
+
_LOG.info(f"{device=}")
|
|
733
|
+
|
|
734
|
+
tgt_primary_key = tgt_stats.get("keys", {}).get("primary_key")
|
|
735
|
+
tgt_context_key = tgt_stats.get("keys", {}).get("context_key")
|
|
736
|
+
ctx_primary_key = ctx_stats.get("keys", {}).get("primary_key")
|
|
737
|
+
_LOG.info(f"{tgt_primary_key=}, {tgt_context_key=}, {ctx_primary_key=}")
|
|
738
|
+
|
|
739
|
+
if rebalancing and isinstance(rebalancing, dict):
|
|
740
|
+
rebalancing = RebalancingConfig(**rebalancing)
|
|
741
|
+
if imputation and isinstance(imputation, dict):
|
|
742
|
+
imputation = ImputationConfig(**imputation)
|
|
743
|
+
if fairness and isinstance(fairness, dict):
|
|
744
|
+
fairness = FairnessConfig(**fairness)
|
|
745
|
+
_LOG.info(f"imputation: {imputation}")
|
|
746
|
+
_LOG.info(f"rebalancing: {rebalancing}")
|
|
747
|
+
_LOG.info(f"fairness: {fairness}")
|
|
748
|
+
_LOG.info(f"sample_seed: {list(seed_data.columns) if isinstance(seed_data, pd.DataFrame) else None}")
|
|
749
|
+
gen_column_order = _resolve_gen_column_order(
|
|
750
|
+
column_stats=tgt_stats["columns"],
|
|
751
|
+
cardinalities=tgt_cardinalities,
|
|
752
|
+
rebalancing=rebalancing,
|
|
753
|
+
imputation=imputation,
|
|
754
|
+
sample_seed=seed_data,
|
|
755
|
+
fairness=fairness,
|
|
756
|
+
)
|
|
757
|
+
_LOG.info(f"{gen_column_order=}")
|
|
758
|
+
|
|
759
|
+
_LOG.info(f"{rare_category_replacement_method=}")
|
|
760
|
+
rare_token_fixed_probs = _fix_rare_token_probs(tgt_stats, rare_category_replacement_method)
|
|
761
|
+
imputation_fixed_probs = _fix_imputation_probs(tgt_stats, imputation)
|
|
762
|
+
rebalancing_fixed_probs = _fix_rebalancing_probs(tgt_stats, rebalancing)
|
|
763
|
+
fixed_probs = _translate_fixed_probs(
|
|
764
|
+
fixed_probs=_deepmerge(
|
|
765
|
+
rare_token_fixed_probs,
|
|
766
|
+
imputation_fixed_probs,
|
|
767
|
+
rebalancing_fixed_probs,
|
|
768
|
+
),
|
|
769
|
+
stats=tgt_stats,
|
|
770
|
+
)
|
|
771
|
+
_LOG.info(f"{sampling_temperature=}, {sampling_top_p=}")
|
|
772
|
+
|
|
773
|
+
if has_context:
|
|
774
|
+
if ctx_data is None:
|
|
775
|
+
# re-use context from training, if no new context provided
|
|
776
|
+
ctx_data = pd.read_parquet(workspace.ctx_data_path)
|
|
777
|
+
_LOG.info(f"generate new data based on context data `{ctx_data.shape}`")
|
|
778
|
+
|
|
779
|
+
# read context input data
|
|
780
|
+
ctx_data = ctx_data.reset_index(drop=True)
|
|
781
|
+
if sample_size is None:
|
|
782
|
+
sample_size = len(ctx_data)
|
|
783
|
+
sample_size = min(sample_size, len(ctx_data))
|
|
784
|
+
|
|
785
|
+
# take first `sample_size` rows of context
|
|
786
|
+
ctx_data = ctx_data.head(sample_size)
|
|
787
|
+
|
|
788
|
+
# validate context data
|
|
789
|
+
ctx_column_stats = list(ctx_stats["columns"].keys())
|
|
790
|
+
missing_columns = [c for c in ctx_column_stats if c not in ctx_data.columns]
|
|
791
|
+
if len(missing_columns) > 0:
|
|
792
|
+
raise ValueError(f"missing columns in provided context data: {', '.join(missing_columns[:5])}")
|
|
793
|
+
else:
|
|
794
|
+
# create on-the-fly context
|
|
795
|
+
if seed_data is None:
|
|
796
|
+
trn_sample_size = tgt_stats["no_of_training_records"] + tgt_stats["no_of_validation_records"]
|
|
797
|
+
sample_size = trn_sample_size if sample_size is None else sample_size
|
|
798
|
+
else: # sample_seed is not None
|
|
799
|
+
sample_size = len(seed_data)
|
|
800
|
+
ctx_primary_key = tgt_context_key or DUMMY_CONTEXT_KEY
|
|
801
|
+
tgt_context_key = ctx_primary_key
|
|
802
|
+
ctx_primary_keys = _generate_primary_keys(sample_size, type="int")
|
|
803
|
+
ctx_primary_keys.rename(ctx_primary_key, inplace=True)
|
|
804
|
+
ctx_data = ctx_primary_keys.to_frame()
|
|
805
|
+
|
|
806
|
+
if seed_data is None:
|
|
807
|
+
# create on-the-fly sample seed
|
|
808
|
+
seed_data = pd.DataFrame(index=range(sample_size))
|
|
809
|
+
|
|
810
|
+
# ensure valid columns in sample_seed
|
|
811
|
+
tgt_columns = list(tgt_stats["columns"].keys()) + ([tgt_primary_key] if tgt_primary_key else [])
|
|
812
|
+
seed_data = seed_data[[c for c in tgt_columns if c in seed_data.columns]]
|
|
813
|
+
|
|
814
|
+
# sequence lengths
|
|
815
|
+
seq_len_stats = get_sequence_length_stats(tgt_stats)
|
|
816
|
+
seq_len_median = seq_len_stats["median"]
|
|
817
|
+
seq_len_min = seq_len_stats["min"]
|
|
818
|
+
seq_len_max = seq_len_stats["max"]
|
|
819
|
+
ctx_seq_len_median = get_ctx_sequence_length(ctx_stats, key="median")
|
|
820
|
+
|
|
821
|
+
# determine batch_size for generation
|
|
822
|
+
if batch_size is None:
|
|
823
|
+
cpu_mem_available_gb = get_available_ram_for_heuristics() / 1024**3
|
|
824
|
+
gpu_mem_available_gb = get_available_vram_for_heuristics() / 1024**3
|
|
825
|
+
batch_size = _generation_batch_size_heuristic(
|
|
826
|
+
mem_available_gb=cpu_mem_available_gb if device.type == "cpu" else gpu_mem_available_gb,
|
|
827
|
+
ctx_stats=ctx_stats,
|
|
828
|
+
tgt_stats=tgt_stats,
|
|
829
|
+
device=device,
|
|
830
|
+
)
|
|
831
|
+
if batch_size < sample_size:
|
|
832
|
+
no_of_batches = int(np.ceil(sample_size / batch_size))
|
|
833
|
+
else:
|
|
834
|
+
no_of_batches = 1
|
|
835
|
+
batch_size = min(batch_size, sample_size)
|
|
836
|
+
_LOG.info(f"{sample_size=}")
|
|
837
|
+
_LOG.info(f"{list(seed_data.columns)=}")
|
|
838
|
+
_LOG.info(f"{batch_size=}")
|
|
839
|
+
_LOG.info(f"{no_of_batches=}")
|
|
840
|
+
|
|
841
|
+
# init progress with total_count; +1 for the final decoding step
|
|
842
|
+
progress.update(completed=0, total=no_of_batches * (seq_len_max + 1))
|
|
843
|
+
|
|
844
|
+
_LOG.info("create generative model")
|
|
845
|
+
model: FlatModel | SequentialModel
|
|
846
|
+
if is_sequential:
|
|
847
|
+
model = SequentialModel(
|
|
848
|
+
tgt_cardinalities=tgt_cardinalities,
|
|
849
|
+
tgt_seq_len_median=seq_len_median,
|
|
850
|
+
tgt_seq_len_max=seq_len_max,
|
|
851
|
+
ctx_cardinalities=ctx_cardinalities,
|
|
852
|
+
ctxseq_len_median=ctx_seq_len_median,
|
|
853
|
+
model_size=model_units,
|
|
854
|
+
column_order=gen_column_order,
|
|
855
|
+
device=device,
|
|
856
|
+
)
|
|
857
|
+
else:
|
|
858
|
+
model = FlatModel(
|
|
859
|
+
tgt_cardinalities=tgt_cardinalities,
|
|
860
|
+
ctx_cardinalities=ctx_cardinalities,
|
|
861
|
+
ctxseq_len_median=ctx_seq_len_median,
|
|
862
|
+
model_size=model_units,
|
|
863
|
+
column_order=gen_column_order,
|
|
864
|
+
device=device,
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
no_of_model_params = get_no_of_model_parameters(model)
|
|
868
|
+
_LOG.info(f"{no_of_model_params=}")
|
|
869
|
+
|
|
870
|
+
load_model_weights(
|
|
871
|
+
model=model,
|
|
872
|
+
path=workspace.model_tabular_weights_path,
|
|
873
|
+
device=device,
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
model.to(device)
|
|
877
|
+
model.eval()
|
|
878
|
+
|
|
879
|
+
# calculate fairness transforms only once before batch generation
|
|
880
|
+
fairness_transforms: FairnessTransforms | None = None
|
|
881
|
+
if fairness and isinstance(model, FlatModel):
|
|
882
|
+
fairness_transforms: FairnessTransforms = get_fairness_transforms(
|
|
883
|
+
fairness=fairness,
|
|
884
|
+
tgt_stats=tgt_stats,
|
|
885
|
+
forward_fn=partial(
|
|
886
|
+
model.forward,
|
|
887
|
+
fixed_probs=fixed_probs,
|
|
888
|
+
temperature=sampling_temperature,
|
|
889
|
+
top_p=sampling_top_p,
|
|
890
|
+
),
|
|
891
|
+
device=device,
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
# resolve encoding types for dtypes harmonisation
|
|
895
|
+
ctx_encoding_types = (
|
|
896
|
+
{c_name: c_data["encoding_type"] for c_name, c_data in ctx_stats["columns"].items()} if has_context else {}
|
|
897
|
+
)
|
|
898
|
+
seed_encoding_types = {
|
|
899
|
+
c_name: c_data["encoding_type"]
|
|
900
|
+
for c_name, c_data in tgt_stats["columns"].items()
|
|
901
|
+
if c_name in seed_data.columns
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
# add __BATCH to ctx_data
|
|
905
|
+
ctx_data = _batch_df(ctx_data, no_of_batches)
|
|
906
|
+
|
|
907
|
+
# add __BATCH to sample_seed
|
|
908
|
+
seed_data = _batch_df(seed_data, no_of_batches)
|
|
909
|
+
|
|
910
|
+
# keep at most 500k samples in memory before decoding and writing to disk
|
|
911
|
+
buffer = FixedSizeSampleBuffer(capacity=500_000)
|
|
912
|
+
|
|
913
|
+
decode_prev_steps = None
|
|
914
|
+
|
|
915
|
+
_LOG.info(f"generate {no_of_batches} batches")
|
|
916
|
+
for batch in range(1, no_of_batches + 1):
|
|
917
|
+
ctx_batch = ctx_data[ctx_data["__BATCH"] == batch]
|
|
918
|
+
ctx_batch = apply_encoding_type_dtypes(ctx_batch, ctx_encoding_types)
|
|
919
|
+
batch_size = len(ctx_batch)
|
|
920
|
+
|
|
921
|
+
seed_batch = seed_data[seed_data["__BATCH"] == batch]
|
|
922
|
+
seed_batch = apply_encoding_type_dtypes(seed_batch, seed_encoding_types)
|
|
923
|
+
|
|
924
|
+
if ctx_primary_key not in ctx_batch.columns:
|
|
925
|
+
ctx_batch[ctx_primary_key] = pd.Series(
|
|
926
|
+
data=_generate_primary_keys(len(ctx_batch), type="int").values,
|
|
927
|
+
index=ctx_batch.index,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
# encode ctx_batch
|
|
931
|
+
_LOG.info(f"encode context {ctx_batch.shape}")
|
|
932
|
+
ctx_batch_encoded, ctx_primary_key_encoded, _ = encode_df(
|
|
933
|
+
df=ctx_batch, stats=ctx_stats, ctx_primary_key=ctx_primary_key
|
|
934
|
+
)
|
|
935
|
+
# pad left context sequences to ensure non-empty sequences
|
|
936
|
+
ctx_batch_encoded = pad_horizontally(ctx_batch_encoded, padding_value=0, right=False)
|
|
937
|
+
ctx_keys = ctx_batch_encoded[ctx_primary_key_encoded]
|
|
938
|
+
ctx_keys.rename(tgt_context_key, inplace=True)
|
|
939
|
+
|
|
940
|
+
# encode seed_batch
|
|
941
|
+
_LOG.info(f"encode sample seed values {seed_batch.shape}")
|
|
942
|
+
seed_batch_encoded, _, _ = encode_df(df=seed_batch, stats=tgt_stats)
|
|
943
|
+
|
|
944
|
+
# sample data from generative model
|
|
945
|
+
_LOG.info(f"sample data from model with context {ctx_batch.shape}")
|
|
946
|
+
if not tgt_sub_columns:
|
|
947
|
+
# there are no columns to sample, emit warning and continue to batch decoding
|
|
948
|
+
_LOG.warning("no target columns to sample")
|
|
949
|
+
syn = ctx_keys.to_frame().reset_index(drop=True)
|
|
950
|
+
buffer.add((syn,))
|
|
951
|
+
elif isinstance(model, SequentialModel):
|
|
952
|
+
ctxflt_inputs = {
|
|
953
|
+
col: torch.unsqueeze(
|
|
954
|
+
torch.as_tensor(ctx_batch_encoded[col].to_numpy(), device=model.device).type(torch.int),
|
|
955
|
+
dim=-1,
|
|
956
|
+
)
|
|
957
|
+
for col in ctx_batch_encoded.columns
|
|
958
|
+
if col.startswith(CTXFLT)
|
|
959
|
+
}
|
|
960
|
+
ctxseq_inputs = {
|
|
961
|
+
col: torch.unsqueeze(
|
|
962
|
+
torch.nested.as_nested_tensor(
|
|
963
|
+
[torch.as_tensor(t, device=model.device).type(torch.int) for t in ctx_batch_encoded[col]],
|
|
964
|
+
device=model.device,
|
|
965
|
+
),
|
|
966
|
+
dim=-1,
|
|
967
|
+
)
|
|
968
|
+
for col in ctx_batch_encoded.columns
|
|
969
|
+
if col.startswith(CTXSEQ)
|
|
970
|
+
}
|
|
971
|
+
seq_steps = model.tgt_seq_len_max
|
|
972
|
+
history = None
|
|
973
|
+
history_state = None
|
|
974
|
+
# process context just once for all sequence steps
|
|
975
|
+
context = model.context_compressor(ctxflt_inputs | ctxseq_inputs)
|
|
976
|
+
# loop over sequence steps, and pass forward history to keep model state-less
|
|
977
|
+
out_dct: dict[str, torch.Tensor] = {}
|
|
978
|
+
decode_prev_steps = {}
|
|
979
|
+
# continue sequences until they reach their predicted length
|
|
980
|
+
step_ctx_keys = ctx_keys
|
|
981
|
+
step_size = batch_size
|
|
982
|
+
step_size_drop_threshold = max(50, batch_size // 100)
|
|
983
|
+
for seq_step in range(seq_steps):
|
|
984
|
+
# exit early if nothing more to sample
|
|
985
|
+
if step_size == 0:
|
|
986
|
+
break
|
|
987
|
+
# fix SIDX by incrementing ourselves instead of sampling
|
|
988
|
+
sidx = pd.Series([seq_step] * step_size)
|
|
989
|
+
sidx_df = encode_slen_sidx_sdec(sidx, max_seq_len=seq_steps, prefix=SIDX_SUB_COLUMN_PREFIX)
|
|
990
|
+
sidx_vals = {
|
|
991
|
+
c: torch.unsqueeze(
|
|
992
|
+
torch.as_tensor(sidx_df[c].to_numpy(), device=model.device).type(torch.int),
|
|
993
|
+
dim=-1,
|
|
994
|
+
)
|
|
995
|
+
for c in sidx_df
|
|
996
|
+
}
|
|
997
|
+
# fix SLEN by propagating sampled SLEN from first step; and update SDEC accordingly
|
|
998
|
+
if seq_step > 0:
|
|
999
|
+
slen_vals = {c: v for c, v in out_dct.items() if c.startswith(SLEN_SUB_COLUMN_PREFIX)}
|
|
1000
|
+
slen = decode_slen_sidx_sdec(
|
|
1001
|
+
pd.DataFrame({c: [x[0].detach().cpu().numpy() for x in v] for c, v in slen_vals.items()}),
|
|
1002
|
+
max_seq_len=seq_steps,
|
|
1003
|
+
prefix=SLEN_SUB_COLUMN_PREFIX,
|
|
1004
|
+
)
|
|
1005
|
+
sdec = (
|
|
1006
|
+
(10 * sidx / slen.clip(lower=1)).clip(upper=9).astype(int)
|
|
1007
|
+
) # sequence index decile; clip as during GENERATE SIDX can become larger than SLEN
|
|
1008
|
+
else:
|
|
1009
|
+
slen_vals = {}
|
|
1010
|
+
sdec = pd.Series([0] * step_size) # initial sequence index decile
|
|
1011
|
+
sdec_vals = {
|
|
1012
|
+
f"{SDEC_SUB_COLUMN_PREFIX}cat": torch.unsqueeze(
|
|
1013
|
+
torch.as_tensor(sdec.to_numpy(), device=model.device).type(torch.int), dim=-1
|
|
1014
|
+
)
|
|
1015
|
+
}
|
|
1016
|
+
fixed_values = sidx_vals | slen_vals | sdec_vals
|
|
1017
|
+
out_dct, history, history_state = model(
|
|
1018
|
+
x=None, # not used in generation forward pass
|
|
1019
|
+
mode="gen",
|
|
1020
|
+
batch_size=step_size,
|
|
1021
|
+
fixed_probs=fixed_probs,
|
|
1022
|
+
fixed_values=fixed_values,
|
|
1023
|
+
temperature=sampling_temperature,
|
|
1024
|
+
top_p=sampling_top_p,
|
|
1025
|
+
history=history,
|
|
1026
|
+
history_state=history_state,
|
|
1027
|
+
context=context,
|
|
1028
|
+
)
|
|
1029
|
+
# transform output dict to tensor for memory efficiency
|
|
1030
|
+
out_pt = torch.stack(list(out_dct.values()), dim=0).transpose(0, 1)
|
|
1031
|
+
# calculate continue sequence mask
|
|
1032
|
+
continue_mask = _continue_sequence_mask(
|
|
1033
|
+
step_output=out_pt,
|
|
1034
|
+
sub_cols=tgt_sub_columns,
|
|
1035
|
+
step_keys=step_ctx_keys,
|
|
1036
|
+
key_name=tgt_context_key,
|
|
1037
|
+
seq_len_min=seq_len_min,
|
|
1038
|
+
seq_len_max=seq_len_max,
|
|
1039
|
+
)
|
|
1040
|
+
next_step_size = continue_mask.sum()
|
|
1041
|
+
# filter next iteration inputs only when threshold is passed
|
|
1042
|
+
# or there is no more data to sample on next iteration
|
|
1043
|
+
if step_size - next_step_size > step_size_drop_threshold or next_step_size == 0:
|
|
1044
|
+
_LOG.info(f"step_size: {step_size} -> {next_step_size}")
|
|
1045
|
+
step_size = next_step_size
|
|
1046
|
+
step_ctx_keys = step_ctx_keys[continue_mask].reset_index(drop=True)
|
|
1047
|
+
out_dct = {k: v[continue_mask, ...] for k, v in out_dct.items()}
|
|
1048
|
+
out_pt = out_pt[continue_mask, ...]
|
|
1049
|
+
# filter context, if it is a sequential context then filter the list of contexts
|
|
1050
|
+
context = [
|
|
1051
|
+
c[continue_mask, ...]
|
|
1052
|
+
if isinstance(c, torch.Tensor)
|
|
1053
|
+
else [sub_c[continue_mask, ...] for sub_c in c]
|
|
1054
|
+
for c in context
|
|
1055
|
+
]
|
|
1056
|
+
history = history[continue_mask, ...]
|
|
1057
|
+
history_state = tuple(h[:, continue_mask, ...] for h in history_state)
|
|
1058
|
+
# accumulate outputs in memory
|
|
1059
|
+
buffer.add((out_pt, step_ctx_keys))
|
|
1060
|
+
# increment progress by 1 for each step
|
|
1061
|
+
progress.update(advance=1)
|
|
1062
|
+
# conditionally decode on step processing end
|
|
1063
|
+
if buffer.is_full():
|
|
1064
|
+
syn = decode_buffered_samples(
|
|
1065
|
+
buffer=buffer,
|
|
1066
|
+
tgt_stats=tgt_stats,
|
|
1067
|
+
tgt_sub_columns=tgt_sub_columns,
|
|
1068
|
+
tgt_primary_key=tgt_primary_key,
|
|
1069
|
+
tgt_context_key=tgt_context_key,
|
|
1070
|
+
decode_prev_steps=decode_prev_steps,
|
|
1071
|
+
)
|
|
1072
|
+
persist_data_part(syn, output_path, f"{buffer.n_clears:06}.{0:06}")
|
|
1073
|
+
buffer.clear()
|
|
1074
|
+
else: # isinstance(model, FlatModel)
|
|
1075
|
+
ctxflt_inputs = {
|
|
1076
|
+
col: torch.unsqueeze(
|
|
1077
|
+
torch.as_tensor(ctx_batch_encoded[col].to_numpy(), device=model.device).type(torch.int),
|
|
1078
|
+
dim=-1,
|
|
1079
|
+
)
|
|
1080
|
+
for col in ctx_batch_encoded.columns
|
|
1081
|
+
if col.startswith(CTXFLT)
|
|
1082
|
+
}
|
|
1083
|
+
ctxseq_inputs = {
|
|
1084
|
+
col: torch.unsqueeze(
|
|
1085
|
+
torch.nested.as_nested_tensor(
|
|
1086
|
+
[torch.as_tensor(t, device=model.device).type(torch.int) for t in ctx_batch_encoded[col]],
|
|
1087
|
+
device=model.device,
|
|
1088
|
+
),
|
|
1089
|
+
dim=-1,
|
|
1090
|
+
)
|
|
1091
|
+
for col in ctx_batch_encoded.columns
|
|
1092
|
+
if col.startswith(CTXSEQ)
|
|
1093
|
+
}
|
|
1094
|
+
x = ctxflt_inputs | ctxseq_inputs
|
|
1095
|
+
fixed_values = {
|
|
1096
|
+
col: torch.as_tensor(seed_batch_encoded[col].to_numpy(), device=model.device).type(torch.int)
|
|
1097
|
+
for col in seed_batch_encoded.columns
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
out_dct, _ = model(
|
|
1101
|
+
x,
|
|
1102
|
+
mode="gen",
|
|
1103
|
+
batch_size=batch_size,
|
|
1104
|
+
fixed_probs=fixed_probs,
|
|
1105
|
+
fixed_values=fixed_values,
|
|
1106
|
+
temperature=sampling_temperature,
|
|
1107
|
+
top_p=sampling_top_p,
|
|
1108
|
+
fairness_transforms=fairness_transforms,
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
syn = pd.concat(
|
|
1112
|
+
[ctx_keys]
|
|
1113
|
+
+ [
|
|
1114
|
+
pd.Series(out_dct[sub_col].detach().cpu().numpy(), dtype="int32", name=sub_col)
|
|
1115
|
+
for sub_col in tgt_cardinalities.keys()
|
|
1116
|
+
],
|
|
1117
|
+
axis=1,
|
|
1118
|
+
)
|
|
1119
|
+
seed_cols_df = seed_batch.loc[:, seed_batch.columns != "__BATCH"].add_prefix("__seed:")
|
|
1120
|
+
if not seed_cols_df.empty:
|
|
1121
|
+
# keep original seed values, in order to preserve them
|
|
1122
|
+
seed_cols_df.reset_index(drop=True, inplace=True)
|
|
1123
|
+
syn = pd.concat([syn, seed_cols_df], axis=1)
|
|
1124
|
+
syn.reset_index(drop=True, inplace=True)
|
|
1125
|
+
buffer.add((syn,))
|
|
1126
|
+
|
|
1127
|
+
# send number of processed batches / steps
|
|
1128
|
+
progress.update(completed=batch * (seq_len_max + 1) - 1)
|
|
1129
|
+
|
|
1130
|
+
# conditionally decode on batch processing end
|
|
1131
|
+
if buffer.is_full():
|
|
1132
|
+
syn = decode_buffered_samples(
|
|
1133
|
+
buffer=buffer,
|
|
1134
|
+
tgt_stats=tgt_stats,
|
|
1135
|
+
tgt_sub_columns=tgt_sub_columns,
|
|
1136
|
+
tgt_primary_key=tgt_primary_key,
|
|
1137
|
+
tgt_context_key=tgt_context_key,
|
|
1138
|
+
decode_prev_steps=decode_prev_steps,
|
|
1139
|
+
)
|
|
1140
|
+
persist_data_part(syn, output_path, f"{buffer.n_clears:06}.{0:06}")
|
|
1141
|
+
buffer.clear()
|
|
1142
|
+
|
|
1143
|
+
progress.update(completed=batch * (seq_len_max + 1))
|
|
1144
|
+
|
|
1145
|
+
# decode before exit if buffer is not empty or seq_len_max is 0
|
|
1146
|
+
if not buffer.is_empty() or seq_len_max == 0:
|
|
1147
|
+
syn = decode_buffered_samples(
|
|
1148
|
+
buffer=buffer,
|
|
1149
|
+
tgt_stats=tgt_stats,
|
|
1150
|
+
tgt_sub_columns=tgt_sub_columns,
|
|
1151
|
+
tgt_primary_key=tgt_primary_key,
|
|
1152
|
+
tgt_context_key=tgt_context_key,
|
|
1153
|
+
decode_prev_steps=decode_prev_steps,
|
|
1154
|
+
)
|
|
1155
|
+
persist_data_part(syn, output_path, f"{buffer.n_clears:06}.{0:06}")
|
|
1156
|
+
buffer.clear()
|
|
1157
|
+
_LOG.info(f"GENERATE_TABULAR finished in {time.time() - t0:.2f}s")
|