tensorless-pytorch 0.2.0__tar.gz → 0.3.0__tar.gz

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 (83) hide show
  1. {tensorless_pytorch-0.2.0/tensorless_pytorch.egg-info → tensorless_pytorch-0.3.0}/PKG-INFO +13 -5
  2. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/README.md +12 -4
  3. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/pyproject.toml +1 -1
  4. tensorless_pytorch-0.3.0/tensorless/auto/config.py +242 -0
  5. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/config.py +8 -0
  6. tensorless_pytorch-0.3.0/tensorless/devices/__init__.py +17 -0
  7. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/devices/device.py +81 -1
  8. tensorless_pytorch-0.3.0/tensorless/models/__init__.py +13 -0
  9. tensorless_pytorch-0.3.0/tensorless/models/mlp.py +142 -0
  10. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/models/registry.py +25 -4
  11. tensorless_pytorch-0.3.0/tensorless/models/transformer.py +491 -0
  12. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/runtime.py +17 -3
  13. tensorless_pytorch-0.3.0/tensorless/tokenization/bpe_tokenizer.py +279 -0
  14. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/training/data_prep.py +46 -16
  15. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/training/trainer.py +51 -8
  16. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0/tensorless_pytorch.egg-info}/PKG-INFO +13 -5
  17. tensorless_pytorch-0.2.0/tensorless/auto/config.py +0 -136
  18. tensorless_pytorch-0.2.0/tensorless/devices/__init__.py +0 -3
  19. tensorless_pytorch-0.2.0/tensorless/models/__init__.py +0 -5
  20. tensorless_pytorch-0.2.0/tensorless/models/mlp.py +0 -64
  21. tensorless_pytorch-0.2.0/tensorless/models/transformer.py +0 -175
  22. tensorless_pytorch-0.2.0/tensorless/tokenization/bpe_tokenizer.py +0 -128
  23. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/LICENSE +0 -0
  24. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/MANIFEST.in +0 -0
  25. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/api_reference.md +0 -0
  26. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/architecture.md +0 -0
  27. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/automatic_mode.md +0 -0
  28. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/checkpointing.md +0 -0
  29. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/cli.md +0 -0
  30. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/configuration.md +0 -0
  31. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/contributing.md +0 -0
  32. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/examples.md +0 -0
  33. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/inference.md +0 -0
  34. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/installation.md +0 -0
  35. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/limitations.md +0 -0
  36. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/quickstart.md +0 -0
  37. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/roadmap.md +0 -0
  38. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/tl_format.md +0 -0
  39. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/training.md +0 -0
  40. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/troubleshooting.md +0 -0
  41. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/docs/tutorial.md +0 -0
  42. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/examples/tabular_classification_example.py +0 -0
  43. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/examples/tabular_regression_example.py +0 -0
  44. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/examples/text_classification_example.py +0 -0
  45. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/examples/text_generation_example.py +0 -0
  46. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/setup.cfg +0 -0
  47. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/__init__.py +0 -0
  48. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/_version.py +0 -0
  49. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/api.py +0 -0
  50. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/auto/__init__.py +0 -0
  51. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/auto/detector.py +0 -0
  52. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/checkpoint/__init__.py +0 -0
  53. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/checkpoint/manager.py +0 -0
  54. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/cli/__init__.py +0 -0
  55. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/cli/main.py +0 -0
  56. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/data/__init__.py +0 -0
  57. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/data/english_grammar.txt +0 -0
  58. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/data/fingerprint.py +0 -0
  59. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/data/inspector.py +0 -0
  60. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/data/loader.py +0 -0
  61. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/data/tabular.py +0 -0
  62. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/errors.py +0 -0
  63. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/serialization/__init__.py +0 -0
  64. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/serialization/tl_format.py +0 -0
  65. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/tokenization/__init__.py +0 -0
  66. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/tokenization/char_tokenizer.py +0 -0
  67. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/training/__init__.py +0 -0
  68. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless/training/early_stopping.py +0 -0
  69. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless_pytorch.egg-info/SOURCES.txt +0 -0
  70. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless_pytorch.egg-info/dependency_links.txt +0 -0
  71. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless_pytorch.egg-info/entry_points.txt +0 -0
  72. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless_pytorch.egg-info/requires.txt +0 -0
  73. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tensorless_pytorch.egg-info/top_level.txt +0 -0
  74. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_auto_detection.py +0 -0
  75. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_checkpoint_resume.py +0 -0
  76. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_cli.py +0 -0
  77. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_data_loading.py +0 -0
  78. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_end_to_end.py +0 -0
  79. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_fingerprint.py +0 -0
  80. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_serialization.py +0 -0
  81. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_train_tabular.py +0 -0
  82. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_train_text_classification.py +0 -0
  83. {tensorless_pytorch-0.2.0 → tensorless_pytorch-0.3.0}/tests/test_train_text_generation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorless-pytorch
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Automatic PyTorch training with portable models and minimal setup.
5
5
  Author: Tensorless PyTorch Contributors
6
6
  License: MIT
@@ -41,10 +41,18 @@ tokenizer; use `tokenizer="char"` for a character-level model. Tensorless PyTorc
41
41
  derives model size, batch size, epochs, validation, device, and BPE vocabulary
42
42
  size from the data, while every setting can be overridden.
43
43
 
44
- Long text is tokenized lazily and fed through PyTorch in fixed-size batches.
45
- CUDA training automatically uses `fp16` or `bf16` when supported, including
46
- gradient scaling and checkpointed scaler state. Reduce `batch_size` if memory
47
- is limited.
44
+ Text is tokenized once up front (not re-tokenized every epoch) and streamed
45
+ through PyTorch in fixed-size batches. Model size is auto-scaled with corpus
46
+ size across four tiers -- small, lower-mid, upper-mid, large -- capped at
47
+ "upper-mid" (roughly 50M-350M parameters) unless you pass explicit
48
+ `d_model=`/`layers=`/etc. yourself. New training runs use the `architecture="v2"`
49
+ backbone (RoPE + RMSNorm + SwiGLU + KV-cached generation); older `.tl`
50
+ checkpoints keep loading and running on the original `"v1"` backbone
51
+ automatically. CUDA training automatically uses `fp16` or `bf16` when
52
+ supported (with gradient scaling and checkpointed scaler state), TPU (XLA)
53
+ training is supported via `device="tpu"`, and large auto-sized models enable
54
+ gradient checkpointing automatically to fit in memory. Reduce `batch_size` if
55
+ memory is limited.
48
56
 
49
57
  ## English starter pretraining
50
58
 
@@ -27,10 +27,18 @@ tokenizer; use `tokenizer="char"` for a character-level model. Tensorless PyTorc
27
27
  derives model size, batch size, epochs, validation, device, and BPE vocabulary
28
28
  size from the data, while every setting can be overridden.
29
29
 
30
- Long text is tokenized lazily and fed through PyTorch in fixed-size batches.
31
- CUDA training automatically uses `fp16` or `bf16` when supported, including
32
- gradient scaling and checkpointed scaler state. Reduce `batch_size` if memory
33
- is limited.
30
+ Text is tokenized once up front (not re-tokenized every epoch) and streamed
31
+ through PyTorch in fixed-size batches. Model size is auto-scaled with corpus
32
+ size across four tiers -- small, lower-mid, upper-mid, large -- capped at
33
+ "upper-mid" (roughly 50M-350M parameters) unless you pass explicit
34
+ `d_model=`/`layers=`/etc. yourself. New training runs use the `architecture="v2"`
35
+ backbone (RoPE + RMSNorm + SwiGLU + KV-cached generation); older `.tl`
36
+ checkpoints keep loading and running on the original `"v1"` backbone
37
+ automatically. CUDA training automatically uses `fp16` or `bf16` when
38
+ supported (with gradient scaling and checkpointed scaler state), TPU (XLA)
39
+ training is supported via `device="tpu"`, and large auto-sized models enable
40
+ gradient checkpointing automatically to fit in memory. Reduce `batch_size` if
41
+ memory is limited.
34
42
 
35
43
  ## English starter pretraining
36
44
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tensorless-pytorch"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Automatic PyTorch training with portable models and minimal setup."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,242 @@
1
+ """Automatic configuration.
2
+
3
+ Turns a `Dataset` + a user-supplied `TrainConfig` (with mostly-None fields)
4
+ into a fully-resolved `ResolvedConfig`. Every automatic choice is driven by
5
+ simple, explainable heuristics based on dataset size and available
6
+ hardware -- this is not meant to be state-of-the-art NAS, it's meant to
7
+ produce a *sane, working* default so `tl.train("./data")` just works,
8
+ scaling all the way from tiny CPU-only experiments up to "upper-mid"
9
+ scale models (roughly 100M-350M parameters for transformers) when the
10
+ data and hardware support it.
11
+
12
+ Model-size tiers (approximate parameter counts, transformer LM):
13
+ small < 10M
14
+ lower-mid 10M - 50M
15
+ upper-mid 50M - 350M <- Tensorless auto-config tops out here
16
+ large 350M - 3B (never auto-selected; pass explicit
17
+ d_model=/layers=/etc. yourself)
18
+ extreme 3B+ (out of scope for this project)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from typing import Any, Dict, Tuple
24
+
25
+ from ..config import TrainConfig, ResolvedConfig
26
+ from ..data.loader import Dataset
27
+ from ..devices.device import auto_select_device, get_torch_device, recommend_max_params
28
+ from .detector import detect_task
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Architecture sizing
33
+ # ---------------------------------------------------------------------------
34
+
35
+ def _text_model_tier(n_examples: int) -> Tuple[int, int, int, int]:
36
+ """Return (d_model, layers, heads, ff_mult), scaled up to the
37
+ "upper-mid" ceiling as effective dataset size grows. `ff_mult` is
38
+ interpreted by the v2 (SwiGLU) architecture as a multiplier on the
39
+ gated hidden width, same as v1's plain MLP hidden width.
40
+ """
41
+ if n_examples < 200:
42
+ return 64, 2, 2, 2 # tiny / smoke-test scale
43
+ elif n_examples < 5_000:
44
+ return 128, 4, 4, 3 # small
45
+ elif n_examples < 50_000:
46
+ return 256, 6, 8, 4 # small / lower-mid boundary
47
+ elif n_examples < 200_000:
48
+ return 384, 8, 8, 4 # lower-mid
49
+ elif n_examples < 1_000_000:
50
+ return 640, 12, 10, 4 # lower-mid / upper-mid boundary (~80M params)
51
+ else:
52
+ return 1024, 16, 16, 4 # upper-mid ceiling (~250-300M params)
53
+
54
+
55
+ def _tabular_model_tier(n_examples: int) -> Tuple[int, int, int, int]:
56
+ if n_examples < 500:
57
+ return 32, 2, 1, 2
58
+ elif n_examples < 20_000:
59
+ return 64, 3, 1, 2
60
+ elif n_examples < 200_000:
61
+ return 128, 5, 1, 2
62
+ else:
63
+ return 256, 8, 1, 2 # upper-mid ceiling for tabular data
64
+
65
+
66
+ def _estimate_transformer_params(vocab_size: int, d_model: int, layers: int, ff_mult: int) -> int:
67
+ swiglu_hidden = max(int(d_model * ff_mult * (2 / 3)), d_model)
68
+ per_layer = 4 * d_model * d_model + 3 * d_model * swiglu_hidden # attn proj + swiglu gate/up/down
69
+ return vocab_size * d_model + layers * per_layer
70
+
71
+
72
+ def _fit_within_param_budget(
73
+ vocab_size: int, d_model: int, layers: int, heads: int, ff_mult: int, max_params: int
74
+ ) -> Tuple[int, int, int, int]:
75
+ """If the tier-selected architecture would exceed what the detected
76
+ hardware can comfortably hold, scale it back down -- first by
77
+ trimming depth, then width -- until it fits. Keeps `d_model`
78
+ divisible by `heads` throughout.
79
+ """
80
+ while _estimate_transformer_params(vocab_size, d_model, layers, ff_mult) > max_params and layers > 2:
81
+ layers -= 1
82
+ while _estimate_transformer_params(vocab_size, d_model, layers, ff_mult) > max_params and d_model > 64:
83
+ d_model = max(64, d_model - 64)
84
+ while d_model % heads != 0 and heads > 1:
85
+ heads -= 1
86
+ return d_model, layers, heads, ff_mult
87
+
88
+
89
+ def _auto_batch_size(n_examples: int, max_seq_len: int, device: str) -> int:
90
+ # GPUs/TPUs benefit from a larger token budget per step than CPU.
91
+ token_budget = 32768 if device in ("cuda", "tpu") else 8192
92
+ sequence_batch = max(1, token_budget // max_seq_len)
93
+ if n_examples < 200:
94
+ return min(8, sequence_batch)
95
+ elif n_examples < 2000:
96
+ return min(16, sequence_batch)
97
+ elif n_examples < 20000:
98
+ return min(32, sequence_batch)
99
+ elif n_examples < 200000:
100
+ return min(64, sequence_batch)
101
+ else:
102
+ return min(128, sequence_batch)
103
+
104
+
105
+ def _auto_epochs(n_examples: int) -> int:
106
+ if n_examples < 200:
107
+ return 40
108
+ elif n_examples < 2000:
109
+ return 20
110
+ elif n_examples < 20000:
111
+ return 10
112
+ else:
113
+ return 5
114
+
115
+
116
+ def _effective_text_size(ds: Dataset) -> int:
117
+ """Estimate useful training examples for raw corpora."""
118
+ if ds.kind in ("text", "text_labeled"):
119
+ return max(len(ds), sum(len(text) for text in ds.texts) // 200)
120
+ return len(ds)
121
+
122
+
123
+ def _auto_vocab_size(ds: Dataset, n_examples: int) -> int:
124
+ if ds.kind not in ("text", "text_labeled"):
125
+ return 1000
126
+ unique_chars = len(set("".join(ds.texts)))
127
+ # Bigger corpora justify (and benefit from) a bigger BPE vocabulary --
128
+ # fewer tokens per sequence, more of max_seq_len spent on real context.
129
+ if n_examples < 5_000:
130
+ ceiling = 2048
131
+ elif n_examples < 200_000:
132
+ ceiling = 8192
133
+ else:
134
+ ceiling = 32000
135
+ return min(ceiling, max(64, unique_chars * 8))
136
+
137
+
138
+ def _auto_num_workers(device: str) -> int:
139
+ import os
140
+
141
+ cpu_count = os.cpu_count() or 1
142
+ if device in ("cuda", "tpu"):
143
+ # Leave headroom for the main process / accelerator driver threads.
144
+ return max(0, min(4, cpu_count - 1))
145
+ return 0 # CPU training: extra worker processes just add overhead
146
+
147
+
148
+ def resolve_config(ds: Dataset, user: TrainConfig) -> ResolvedConfig:
149
+ n = _effective_text_size(ds)
150
+ task = user.task or detect_task(ds)
151
+ model_type = user.model_type or (
152
+ "transformer" if task in ("text-generation", "text-classification") else "mlp"
153
+ )
154
+ tokenizer = user.tokenizer or "bpe"
155
+ if tokenizer not in ("char", "bpe"):
156
+ raise ValueError("tokenizer must be 'char' or 'bpe'")
157
+
158
+ architecture = user.architecture or "v2"
159
+ if architecture not in ("v1", "v2"):
160
+ raise ValueError("architecture must be 'v1' or 'v2'")
161
+
162
+ device, precision = auto_select_device(user.device, user.precision)
163
+ torch_device = get_torch_device(device)
164
+
165
+ if model_type == "transformer":
166
+ d_model, layers, heads, ff_mult = _text_model_tier(n)
167
+ else:
168
+ d_model, layers, heads, ff_mult = _tabular_model_tier(n)
169
+
170
+ max_seq_len = user.max_seq_len or (256 if ds.kind in ("text", "text_labeled") else 1)
171
+ bpe_vocab_size = (
172
+ user.bpe_vocab_size if user.bpe_vocab_size is not None else _auto_vocab_size(ds, n)
173
+ )
174
+
175
+ # Memory-aware cap: only clamp automatically-chosen sizes, never a
176
+ # user's explicit d_model=/layers=/heads=/ff_mult= override.
177
+ user_set_arch = any(
178
+ v is not None for v in (user.d_model, user.layers, user.heads, user.ff_mult)
179
+ )
180
+ if model_type == "transformer" and not user_set_arch:
181
+ max_params = recommend_max_params(torch_device)
182
+ d_model, layers, heads, ff_mult = _fit_within_param_budget(
183
+ bpe_vocab_size if tokenizer == "bpe" else 256, d_model, layers, heads, ff_mult, max_params
184
+ )
185
+
186
+ gradient_checkpointing = user.gradient_checkpointing
187
+ if gradient_checkpointing is None:
188
+ # Auto-enable on GPU/TPU once the model is large enough that
189
+ # activation memory (not parameter memory) becomes the bottleneck.
190
+ approx_params = _estimate_transformer_params(
191
+ bpe_vocab_size if tokenizer == "bpe" else 256, d_model, layers, ff_mult
192
+ ) if model_type == "transformer" else 0
193
+ gradient_checkpointing = device in ("cuda", "tpu") and approx_params > 60_000_000
194
+
195
+ compile_model = user.compile
196
+ if compile_model is None:
197
+ compile_model = device == "cuda"
198
+
199
+ num_workers = user.num_workers if user.num_workers is not None else _auto_num_workers(device)
200
+
201
+ out = user.out or "model.tl"
202
+ checkpoint_dir = user.checkpoint_dir or (out + ".ckpt")
203
+
204
+ resolved = ResolvedConfig(
205
+ out=out,
206
+ force=bool(user.force),
207
+ resume=user.resume,
208
+ ask_on_data_change=bool(user.ask_on_data_change),
209
+ task=task,
210
+ model_type=model_type,
211
+ architecture=architecture,
212
+ d_model=user.d_model or d_model,
213
+ layers=user.layers or layers,
214
+ heads=user.heads or heads,
215
+ ff_mult=user.ff_mult or ff_mult,
216
+ dropout=user.dropout if user.dropout is not None else 0.1,
217
+ max_seq_len=max_seq_len,
218
+ tokenizer=tokenizer,
219
+ bpe_vocab_size=bpe_vocab_size,
220
+ optimizer=user.optimizer or "adamw",
221
+ learning_rate=user.learning_rate or (3e-4 if model_type == "transformer" else 1e-3),
222
+ weight_decay=user.weight_decay if user.weight_decay is not None else 0.01,
223
+ batch_size=user.batch_size if user.batch_size is not None else _auto_batch_size(n, max_seq_len, device),
224
+ epochs=user.epochs or _auto_epochs(n),
225
+ max_steps=user.max_steps,
226
+ gradient_accumulation_steps=user.gradient_accumulation_steps or 1,
227
+ grad_clip=user.grad_clip if user.grad_clip is not None else 1.0,
228
+ warmup_steps=user.warmup_steps if user.warmup_steps is not None else min(100, max(1, n // 10)),
229
+ val_split=user.val_split if user.val_split is not None else (0.1 if n >= 50 else 0.0),
230
+ patience=user.patience if user.patience is not None else 3,
231
+ min_delta=user.min_delta if user.min_delta is not None else 1e-4,
232
+ device=device,
233
+ precision=precision,
234
+ checkpoint_every=user.checkpoint_every or 50,
235
+ checkpoint_dir=checkpoint_dir,
236
+ gradient_checkpointing=gradient_checkpointing,
237
+ compile=compile_model,
238
+ num_workers=num_workers,
239
+ seed=user.seed,
240
+ verbose=user.verbose,
241
+ )
242
+ return resolved
@@ -27,6 +27,7 @@ class TrainConfig:
27
27
  # --- task / architecture ---
28
28
  task: Optional[str] = None # "text-generation", "classification", "regression"
29
29
  model_type: Optional[str] = None # "transformer", "mlp"
30
+ architecture: Optional[str] = None # "v1" (legacy) or "v2" (RoPE/RMSNorm/SwiGLU); default "v2"
30
31
  d_model: Optional[int] = None
31
32
  layers: Optional[int] = None
32
33
  heads: Optional[int] = None
@@ -35,6 +36,9 @@ class TrainConfig:
35
36
  max_seq_len: Optional[int] = None
36
37
  tokenizer: Optional[str] = None # "char" or "bpe"
37
38
  bpe_vocab_size: Optional[int] = None
39
+ gradient_checkpointing: Optional[bool] = None # trade compute for memory on big models
40
+ compile: Optional[bool] = None # torch.compile() on CUDA when available
41
+ num_workers: Optional[int] = None # DataLoader worker processes
38
42
 
39
43
  # --- optimization ---
40
44
  optimizer: Optional[str] = None # "adamw", "adam", "sgd"
@@ -86,6 +90,7 @@ class ResolvedConfig:
86
90
 
87
91
  task: str
88
92
  model_type: str
93
+ architecture: str
89
94
  d_model: int
90
95
  layers: int
91
96
  heads: int
@@ -94,6 +99,9 @@ class ResolvedConfig:
94
99
  max_seq_len: int
95
100
  tokenizer: str
96
101
  bpe_vocab_size: int
102
+ gradient_checkpointing: bool
103
+ compile: bool
104
+ num_workers: int
97
105
 
98
106
  optimizer: str
99
107
  learning_rate: float
@@ -0,0 +1,17 @@
1
+ from .device import (
2
+ auto_select_device,
3
+ get_torch_device,
4
+ get_device_info,
5
+ mark_step,
6
+ recommend_max_params,
7
+ tpu_core_count,
8
+ )
9
+
10
+ __all__ = [
11
+ "auto_select_device",
12
+ "get_torch_device",
13
+ "get_device_info",
14
+ "mark_step",
15
+ "recommend_max_params",
16
+ "tpu_core_count",
17
+ ]
@@ -9,7 +9,8 @@ trainer (see `training/trainer.py`) will catch that and fall back too.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
- from typing import Optional, Tuple
12
+ import os
13
+ from typing import Any, Dict, Optional, Tuple
13
14
 
14
15
  import torch
15
16
 
@@ -85,6 +86,85 @@ def auto_select_device(user_device: Optional[str], user_precision: Optional[str]
85
86
  return device, precision
86
87
 
87
88
 
89
+ def tpu_core_count() -> int:
90
+ """Number of TPU cores visible to this process (1 if not on TPU)."""
91
+ try:
92
+ import torch_xla.core.xla_model as xm
93
+
94
+ return xm.xrt_world_size()
95
+ except Exception:
96
+ try:
97
+ import torch_xla.runtime as xr
98
+
99
+ return xr.world_size()
100
+ except Exception:
101
+ return 1
102
+
103
+
104
+ def mark_step(device: torch.device) -> None:
105
+ """XLA/TPU tensors are lazily evaluated -- without periodically calling
106
+ `xm.mark_step()` the compute graph never actually executes and a
107
+ 'TPU-enabled' training run will just silently build up an ever-larger
108
+ graph (or hang) instead of doing real work. This is a no-op on every
109
+ other device, so it's safe to call unconditionally from the trainer.
110
+ """
111
+ if device.type == "xla":
112
+ try:
113
+ import torch_xla.core.xla_model as xm
114
+
115
+ xm.mark_step()
116
+ except Exception:
117
+ pass
118
+
119
+
120
+ def get_device_info(device: torch.device) -> Dict[str, Any]:
121
+ """Human-readable + programmatic info about the active compute device,
122
+ used both for the startup log line and for memory-aware auto-sizing.
123
+ """
124
+ info: Dict[str, Any] = {"type": device.type, "name": device.type, "memory_gb": None, "num_devices": 1}
125
+ try:
126
+ if device.type == "cuda":
127
+ idx = device.index if device.index is not None else 0
128
+ props = torch.cuda.get_device_properties(idx)
129
+ info["name"] = props.name
130
+ info["memory_gb"] = round(props.total_memory / (1024 ** 3), 2)
131
+ info["num_devices"] = torch.cuda.device_count()
132
+ info["bf16"] = _cuda_supports_bf16()
133
+ elif device.type == "xla":
134
+ info["name"] = "TPU"
135
+ info["num_devices"] = tpu_core_count()
136
+ elif device.type == "mps":
137
+ info["name"] = "Apple Silicon (MPS)"
138
+ else:
139
+ info["name"] = "CPU"
140
+ info["num_devices"] = os.cpu_count() or 1
141
+ except Exception:
142
+ pass
143
+ return info
144
+
145
+
146
+ def recommend_max_params(device: torch.device) -> int:
147
+ """Rough ceiling on trainable parameter count so an auto-configured
148
+ model has a reasonable chance of fitting in memory alongside AdamW's
149
+ optimizer state (~2x params) and activations. Deliberately
150
+ conservative -- users can always override architecture args by hand.
151
+ """
152
+ info = get_device_info(device)
153
+ if device.type == "cuda" and info.get("memory_gb"):
154
+ # Rule of thumb for AdamW + fp16/bf16 mixed precision + activations:
155
+ # budget roughly 1M params per 15MB of GPU memory, capped well
156
+ # inside the "upper-mid" tier regardless of how much memory exists.
157
+ budget = int(info["memory_gb"] * 1024 * (1_000_000 / 15))
158
+ return max(2_000_000, min(budget, 350_000_000))
159
+ if device.type == "xla":
160
+ # TPU v3/v4 cores typically expose >=16GB HBM each.
161
+ return 350_000_000
162
+ if device.type == "mps":
163
+ return 60_000_000
164
+ # CPU: keep it trainable in a reasonable amount of wall-clock time.
165
+ return 40_000_000
166
+
167
+
88
168
  def get_torch_device(device: str) -> torch.device:
89
169
  """Convert our string device name into a torch.device, with a
90
170
  runtime fallback to CPU if the requested backend is unavailable.
@@ -0,0 +1,13 @@
1
+ from .registry import build_model
2
+ from .transformer import TinyTransformer, TinyTransformerV1, TinyTransformerV2
3
+ from .mlp import TabularMLP, TabularMLPV1, TabularMLPV2
4
+
5
+ __all__ = [
6
+ "build_model",
7
+ "TinyTransformer",
8
+ "TinyTransformerV1",
9
+ "TinyTransformerV2",
10
+ "TabularMLP",
11
+ "TabularMLPV1",
12
+ "TabularMLPV2",
13
+ ]
@@ -0,0 +1,142 @@
1
+ """MLP model for tabular data (classification / regression).
2
+
3
+ Numeric columns feed directly into the network; each categorical column
4
+ gets its own small embedding table, and all features are concatenated
5
+ before the hidden layers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import List
11
+
12
+ import torch
13
+ import torch.nn as nn
14
+ import torch.nn.functional as F
15
+
16
+
17
+ def _embedding_dim(vocab_size: int) -> int:
18
+ # Common rule of thumb, capped to keep tiny-data models tiny.
19
+ return max(2, min(32, round(1.6 * (vocab_size ** 0.56))))
20
+
21
+
22
+ class TabularMLPV1(nn.Module):
23
+ def __init__(
24
+ self,
25
+ n_numeric: int,
26
+ categorical_vocab_sizes: List[int],
27
+ d_model: int,
28
+ layers: int,
29
+ dropout: float,
30
+ task: str,
31
+ n_classes: int = 0,
32
+ ):
33
+ super().__init__()
34
+ self.task = task
35
+ self.n_numeric = n_numeric
36
+ self.categorical_vocab_sizes = categorical_vocab_sizes
37
+
38
+ self.embeddings = nn.ModuleList(
39
+ [nn.Embedding(v, _embedding_dim(v)) for v in categorical_vocab_sizes]
40
+ )
41
+ cat_dim = sum(_embedding_dim(v) for v in categorical_vocab_sizes)
42
+ in_dim = n_numeric + cat_dim
43
+
44
+ dims = [in_dim] + [d_model] * layers
45
+ modules = []
46
+ for i in range(len(dims) - 1):
47
+ modules += [nn.Linear(dims[i], dims[i + 1]), nn.ReLU(), nn.Dropout(dropout)]
48
+ self.backbone = nn.Sequential(*modules)
49
+
50
+ out_dim = n_classes if task == "classification" else 1
51
+ self.head = nn.Linear(dims[-1], out_dim)
52
+
53
+ def forward(self, numeric: torch.Tensor, categorical: torch.Tensor) -> torch.Tensor:
54
+ parts = []
55
+ if self.n_numeric > 0:
56
+ parts.append(numeric)
57
+ for i, emb in enumerate(self.embeddings):
58
+ parts.append(emb(categorical[:, i]))
59
+ x = torch.cat(parts, dim=1) if parts else numeric
60
+ x = self.backbone(x)
61
+ out = self.head(x)
62
+ if self.task == "regression":
63
+ return out.squeeze(-1)
64
+ return out
65
+
66
+
67
+ class _ResidualBlock(nn.Module):
68
+ """Pre-norm residual MLP block (LayerNorm -> Linear -> GELU -> Linear
69
+ -> residual add). Deeper `TabularMLPV2` stacks of these train more
70
+ stably than plain stacked Linear+ReLU at larger widths/depths.
71
+ """
72
+
73
+ def __init__(self, d_model: int, dropout: float):
74
+ super().__init__()
75
+ self.norm = nn.LayerNorm(d_model)
76
+ self.fc1 = nn.Linear(d_model, d_model)
77
+ self.fc2 = nn.Linear(d_model, d_model)
78
+ self.drop = nn.Dropout(dropout)
79
+
80
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
81
+ h = self.norm(x)
82
+ h = F.gelu(self.fc1(h))
83
+ h = self.drop(h)
84
+ h = self.fc2(h)
85
+ return x + self.drop(h)
86
+
87
+
88
+ class TabularMLPV2(nn.Module):
89
+ """Residual, pre-norm tabular MLP -- same public interface as
90
+ `TabularMLPV1`, but scales more gracefully to the wider/deeper configs
91
+ Tensorless now auto-selects for larger tabular datasets.
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ n_numeric: int,
97
+ categorical_vocab_sizes: List[int],
98
+ d_model: int,
99
+ layers: int,
100
+ dropout: float,
101
+ task: str,
102
+ n_classes: int = 0,
103
+ ):
104
+ super().__init__()
105
+ self.task = task
106
+ self.n_numeric = n_numeric
107
+ self.categorical_vocab_sizes = categorical_vocab_sizes
108
+
109
+ self.embeddings = nn.ModuleList(
110
+ [nn.Embedding(v, _embedding_dim(v)) for v in categorical_vocab_sizes]
111
+ )
112
+ cat_dim = sum(_embedding_dim(v) for v in categorical_vocab_sizes)
113
+ in_dim = n_numeric + cat_dim
114
+
115
+ self.in_proj = nn.Linear(in_dim, d_model)
116
+ self.blocks = nn.ModuleList([_ResidualBlock(d_model, dropout) for _ in range(layers)])
117
+ self.norm_f = nn.LayerNorm(d_model)
118
+
119
+ out_dim = n_classes if task == "classification" else 1
120
+ self.head = nn.Linear(d_model, out_dim)
121
+
122
+ def forward(self, numeric: torch.Tensor, categorical: torch.Tensor) -> torch.Tensor:
123
+ parts = []
124
+ if self.n_numeric > 0:
125
+ parts.append(numeric)
126
+ for i, emb in enumerate(self.embeddings):
127
+ parts.append(emb(categorical[:, i]))
128
+ x = torch.cat(parts, dim=1) if parts else numeric
129
+ x = self.in_proj(x)
130
+ for block in self.blocks:
131
+ x = block(x)
132
+ x = self.norm_f(x)
133
+ out = self.head(x)
134
+ if self.task == "regression":
135
+ return out.squeeze(-1)
136
+ return out
137
+
138
+
139
+ # New training runs default to the residual v2 architecture; v1 stays
140
+ # available (and is what old checkpoints without an "architecture" key
141
+ # in their config resolve to) for backward compatibility.
142
+ TabularMLP = TabularMLPV2
@@ -5,6 +5,14 @@ config + the dataset-derived metadata (vocab size, n_classes, etc). Kept
5
5
  separate from `auto/config.py` so new model types/backends can be added
6
6
  here without touching auto-configuration logic, per the "extensible
7
7
  architecture" requirement.
8
+
9
+ Also owns the v1/v2 architecture switch: `cfg.get("architecture", "v1")`
10
+ decides which concrete class gets built. Checkpoints saved before the v2
11
+ upgrade have no "architecture" key in their stored config at all, so
12
+ `.get(..., "v1")` transparently resolves them to the original
13
+ implementation -- old `.tl` files keep loading and running correctly.
14
+ New training runs get "v2" (better quality, KV-cache generation, scales
15
+ to "upper-mid" sizes) unless the user explicitly pins `architecture="v1"`.
8
16
  """
9
17
 
10
18
  from __future__ import annotations
@@ -13,8 +21,8 @@ from typing import Any, Dict
13
21
 
14
22
  import torch.nn as nn
15
23
 
16
- from .transformer import TinyTransformer
17
- from .mlp import TabularMLP
24
+ from .transformer import TinyTransformerV1, TinyTransformerV2
25
+ from .mlp import TabularMLPV1, TabularMLPV2
18
26
  from ..errors import ModelError
19
27
 
20
28
 
@@ -26,8 +34,10 @@ def build_model(task: str, model_type: str, cfg: Dict[str, Any], meta: Dict[str,
26
34
  - text tasks: {"vocab_size": int, "pad_id": int, "n_classes": int}
27
35
  - tabular tasks: {"n_numeric": int, "categorical_vocab_sizes": [...], "n_classes": int}
28
36
  """
37
+ architecture = cfg.get("architecture", "v1")
38
+
29
39
  if model_type == "transformer":
30
- return TinyTransformer(
40
+ common = dict(
31
41
  vocab_size=meta["vocab_size"],
32
42
  d_model=cfg["d_model"],
33
43
  layers=cfg["layers"],
@@ -39,8 +49,15 @@ def build_model(task: str, model_type: str, cfg: Dict[str, Any], meta: Dict[str,
39
49
  n_classes=meta.get("n_classes", 0),
40
50
  pad_id=meta.get("pad_id", 0),
41
51
  )
52
+ if architecture == "v2":
53
+ return TinyTransformerV2(
54
+ gradient_checkpointing=bool(cfg.get("gradient_checkpointing", False)),
55
+ **common,
56
+ )
57
+ return TinyTransformerV1(**common)
58
+
42
59
  elif model_type == "mlp":
43
- return TabularMLP(
60
+ common = dict(
44
61
  n_numeric=meta["n_numeric"],
45
62
  categorical_vocab_sizes=meta["categorical_vocab_sizes"],
46
63
  d_model=cfg["d_model"],
@@ -49,5 +66,9 @@ def build_model(task: str, model_type: str, cfg: Dict[str, Any], meta: Dict[str,
49
66
  task=task,
50
67
  n_classes=meta.get("n_classes", 0),
51
68
  )
69
+ if architecture == "v2":
70
+ return TabularMLPV2(**common)
71
+ return TabularMLPV1(**common)
72
+
52
73
  else:
53
74
  raise ModelError(f"Unknown model_type '{model_type}'.")