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.
Files changed (45) hide show
  1. mostlyai/engine/__init__.py +27 -0
  2. mostlyai/engine/_common.py +612 -0
  3. mostlyai/engine/_dtypes.py +58 -0
  4. mostlyai/engine/_encoding_types/__init__.py +13 -0
  5. mostlyai/engine/_encoding_types/language/__init__.py +13 -0
  6. mostlyai/engine/_encoding_types/language/text.py +41 -0
  7. mostlyai/engine/_encoding_types/tabular/__init__.py +13 -0
  8. mostlyai/engine/_encoding_types/tabular/categorical.py +115 -0
  9. mostlyai/engine/_encoding_types/tabular/character.py +155 -0
  10. mostlyai/engine/_encoding_types/tabular/datetime.py +254 -0
  11. mostlyai/engine/_encoding_types/tabular/itt.py +352 -0
  12. mostlyai/engine/_encoding_types/tabular/lat_long.py +353 -0
  13. mostlyai/engine/_encoding_types/tabular/numeric.py +548 -0
  14. mostlyai/engine/_language/__init__.py +17 -0
  15. mostlyai/engine/_language/common.py +110 -0
  16. mostlyai/engine/_language/encoding.py +162 -0
  17. mostlyai/engine/_language/engine/__init__.py +13 -0
  18. mostlyai/engine/_language/engine/base.py +51 -0
  19. mostlyai/engine/_language/engine/hf_engine.py +136 -0
  20. mostlyai/engine/_language/engine/vllm_engine.py +187 -0
  21. mostlyai/engine/_language/formatron_utils.py +114 -0
  22. mostlyai/engine/_language/generation.py +309 -0
  23. mostlyai/engine/_language/lstm.py +139 -0
  24. mostlyai/engine/_language/tokenizer_utils.py +144 -0
  25. mostlyai/engine/_language/training.py +718 -0
  26. mostlyai/engine/_memory.py +69 -0
  27. mostlyai/engine/_tabular/__init__.py +13 -0
  28. mostlyai/engine/_tabular/argn.py +1382 -0
  29. mostlyai/engine/_tabular/common.py +29 -0
  30. mostlyai/engine/_tabular/encoding.py +406 -0
  31. mostlyai/engine/_tabular/fairness.py +245 -0
  32. mostlyai/engine/_tabular/generation.py +1157 -0
  33. mostlyai/engine/_tabular/training.py +856 -0
  34. mostlyai/engine/_training_utils.py +159 -0
  35. mostlyai/engine/_workspace.py +225 -0
  36. mostlyai/engine/analysis.py +571 -0
  37. mostlyai/engine/domain.py +171 -0
  38. mostlyai/engine/encoding.py +42 -0
  39. mostlyai/engine/generation.py +110 -0
  40. mostlyai/engine/splitting.py +257 -0
  41. mostlyai/engine/training.py +104 -0
  42. mostlyai_engine-1.0.0.dist-info/METADATA +179 -0
  43. mostlyai_engine-1.0.0.dist-info/RECORD +45 -0
  44. mostlyai_engine-1.0.0.dist-info/WHEEL +4 -0
  45. 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")