tensorless 0.5.0__tar.gz → 0.6.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 (85) hide show
  1. {tensorless-0.5.0/tensorless.egg-info → tensorless-0.6.0}/PKG-INFO +2 -1
  2. {tensorless-0.5.0 → tensorless-0.6.0}/README.md +2 -1
  3. {tensorless-0.5.0 → tensorless-0.6.0}/docs/checkpointing.md +1 -0
  4. {tensorless-0.5.0 → tensorless-0.6.0}/pyproject.toml +1 -1
  5. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/backends/jax_backend.py +11 -2
  6. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/backends/mlx_backend.py +11 -2
  7. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/engine.py +45 -7
  8. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/models/transformer.py +65 -196
  9. tensorless-0.6.0/tensorless/training/trainer.py +138 -0
  10. {tensorless-0.5.0 → tensorless-0.6.0/tensorless.egg-info}/PKG-INFO +2 -1
  11. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_train_tabular.py +11 -0
  12. tensorless-0.5.0/tensorless/training/trainer.py +0 -369
  13. {tensorless-0.5.0 → tensorless-0.6.0}/LICENSE +0 -0
  14. {tensorless-0.5.0 → tensorless-0.6.0}/MANIFEST.in +0 -0
  15. {tensorless-0.5.0 → tensorless-0.6.0}/docs/api_reference.md +0 -0
  16. {tensorless-0.5.0 → tensorless-0.6.0}/docs/architecture.md +0 -0
  17. {tensorless-0.5.0 → tensorless-0.6.0}/docs/automatic_mode.md +0 -0
  18. {tensorless-0.5.0 → tensorless-0.6.0}/docs/cli.md +0 -0
  19. {tensorless-0.5.0 → tensorless-0.6.0}/docs/configuration.md +0 -0
  20. {tensorless-0.5.0 → tensorless-0.6.0}/docs/contributing.md +0 -0
  21. {tensorless-0.5.0 → tensorless-0.6.0}/docs/examples.md +0 -0
  22. {tensorless-0.5.0 → tensorless-0.6.0}/docs/inference.md +0 -0
  23. {tensorless-0.5.0 → tensorless-0.6.0}/docs/installation.md +0 -0
  24. {tensorless-0.5.0 → tensorless-0.6.0}/docs/limitations.md +0 -0
  25. {tensorless-0.5.0 → tensorless-0.6.0}/docs/quickstart.md +0 -0
  26. {tensorless-0.5.0 → tensorless-0.6.0}/docs/roadmap.md +0 -0
  27. {tensorless-0.5.0 → tensorless-0.6.0}/docs/tl_format.md +0 -0
  28. {tensorless-0.5.0 → tensorless-0.6.0}/docs/training.md +0 -0
  29. {tensorless-0.5.0 → tensorless-0.6.0}/docs/troubleshooting.md +0 -0
  30. {tensorless-0.5.0 → tensorless-0.6.0}/docs/tutorial.md +0 -0
  31. {tensorless-0.5.0 → tensorless-0.6.0}/examples/tabular_classification_example.py +0 -0
  32. {tensorless-0.5.0 → tensorless-0.6.0}/examples/tabular_regression_example.py +0 -0
  33. {tensorless-0.5.0 → tensorless-0.6.0}/examples/text_classification_example.py +0 -0
  34. {tensorless-0.5.0 → tensorless-0.6.0}/examples/text_generation_example.py +0 -0
  35. {tensorless-0.5.0 → tensorless-0.6.0}/setup.cfg +0 -0
  36. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/__init__.py +0 -0
  37. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/_version.py +0 -0
  38. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/api.py +0 -0
  39. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/auto/__init__.py +0 -0
  40. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/auto/config.py +0 -0
  41. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/auto/detector.py +0 -0
  42. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/backends/__init__.py +0 -0
  43. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/checkpoint/__init__.py +0 -0
  44. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/checkpoint/manager.py +0 -0
  45. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/cli/__init__.py +0 -0
  46. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/cli/main.py +0 -0
  47. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/config.py +0 -0
  48. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/data/__init__.py +0 -0
  49. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/data/english_grammar.txt +0 -0
  50. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/data/fingerprint.py +0 -0
  51. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/data/inspector.py +0 -0
  52. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/data/loader.py +0 -0
  53. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/data/tabular.py +0 -0
  54. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/devices/__init__.py +0 -0
  55. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/devices/device.py +0 -0
  56. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/devices/memory.py +0 -0
  57. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/errors.py +0 -0
  58. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/models/__init__.py +0 -0
  59. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/models/mlp.py +0 -0
  60. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/models/registry.py +0 -0
  61. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/runtime.py +0 -0
  62. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/serialization/__init__.py +0 -0
  63. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/serialization/tl_format.py +0 -0
  64. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/tokenization/__init__.py +0 -0
  65. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/tokenization/bpe_tokenizer.py +0 -0
  66. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/tokenization/char_tokenizer.py +0 -0
  67. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/training/__init__.py +0 -0
  68. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/training/data_prep.py +0 -0
  69. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless/training/early_stopping.py +0 -0
  70. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless.egg-info/SOURCES.txt +0 -0
  71. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless.egg-info/dependency_links.txt +0 -0
  72. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless.egg-info/entry_points.txt +0 -0
  73. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless.egg-info/requires.txt +0 -0
  74. {tensorless-0.5.0 → tensorless-0.6.0}/tensorless.egg-info/top_level.txt +0 -0
  75. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_auto_detection.py +0 -0
  76. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_checkpoint_resume.py +0 -0
  77. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_cli.py +0 -0
  78. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_data_loading.py +0 -0
  79. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_end_to_end.py +0 -0
  80. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_fingerprint.py +0 -0
  81. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_jax_backend.py +0 -0
  82. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_mlx_backend.py +0 -0
  83. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_serialization.py +0 -0
  84. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_train_text_classification.py +0 -0
  85. {tensorless-0.5.0 → tensorless-0.6.0}/tests/test_train_text_generation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorless
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: ML with maximum automation and minimum setup.
5
5
  Author: Tensorless Contributors
6
6
  License: MIT
@@ -105,3 +105,4 @@ without a PyTorch dependency. Accelerator cache helpers are available as
105
105
 
106
106
  See the [documentation](docs/quickstart.md) for data formats, configuration,
107
107
  checkpointing, and the command-line interface.
108
+ pypi-AgEIcHlwaS5vcmcCJDJmYzZiYmQ1LTI1YTAtNGNlZi05OWE2LTliMjg3ZjY5MThiZQACElsxLFsidGVuc29ybGVzcyJdXQACLFsyLFsiNjcxZGZmNDQtZmVmMC00MWNiLWIzYzYtZWQzMTI3OWU4NWM5Il1dAAAGIDZAp3AAlp0idrTOMPJ227qF_7W0LDsyUpi6pfqf0aV0
@@ -78,4 +78,5 @@ without a PyTorch dependency. Accelerator cache helpers are available as
78
78
  `tensorless.devices.clear_memory()` and `tensorless.devices.memory_stats()`.
79
79
 
80
80
  See the [documentation](docs/quickstart.md) for data formats, configuration,
81
- checkpointing, and the command-line interface.
81
+ checkpointing, and the command-line interface.
82
+ pypi-AgEIcHlwaS5vcmcCJDJmYzZiYmQ1LTI1YTAtNGNlZi05OWE2LTliMjg3ZjY5MThiZQACElsxLFsidGVuc29ybGVzcyJdXQACLFsyLFsiNjcxZGZmNDQtZmVmMC00MWNiLWIzYzYtZWQzMTI3OWU4NWM5Il1dAAAGIDZAp3AAlp0idrTOMPJ227qF_7W0LDsyUpi6pfqf0aV0
@@ -16,6 +16,7 @@ create or manage this directory yourself.
16
16
  - `optimizer_state_dict` — optimizer momentum/variance buffers
17
17
  - `scheduler_state_dict` — learning rate schedule position
18
18
  - `epoch`, `global_step` — where training left off
19
+ - `train_loader_epoch` — deterministic shuffle position for resumed training
19
20
  - `early_stopping_best`, `early_stopping_bad_checks` — early stopping state
20
21
  - `config` — the fully resolved training configuration used
21
22
  - `meta` — task-specific sizing info (vocab size, number of classes, etc.)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tensorless"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "ML with maximum automation and minimum setup."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -58,13 +58,20 @@ class JaxTinyTransformer(TinyTransformer):
58
58
 
59
59
  def _forward_jax(self, params, input_ids, attention_mask=None):
60
60
  _, jnp = _jax()
61
+
62
+ def layer_norm(x, gain, bias, eps=1e-5):
63
+ mean = jnp.mean(x, axis=-1, keepdims=True)
64
+ var = jnp.var(x, axis=-1, keepdims=True)
65
+ return (x - mean) / jnp.sqrt(var + eps) * gain + bias
66
+
61
67
  input_ids = jnp.asarray(input_ids, dtype=jnp.int32)
62
68
  batch, length = input_ids.shape
63
69
  hidden = params["tok_emb"][input_ids] + params["pos_emb"][jnp.arange(length)]
64
70
  causal = jnp.tril(jnp.ones((length, length), dtype=bool))
65
71
  for index in range(len(self.blocks)):
66
72
  prefix = f"blocks.{index}"
67
- qkv = hidden @ params[f"{prefix}.qkv_weight"] + params[f"{prefix}.qkv_bias"]
73
+ normed1 = layer_norm(hidden, params[f"{prefix}.ln1_gain"], params[f"{prefix}.ln1_bias"])
74
+ qkv = normed1 @ params[f"{prefix}.qkv_weight"] + params[f"{prefix}.qkv_bias"]
68
75
  q, k, v = jnp.split(qkv, 3, axis=-1)
69
76
  q = q.reshape(batch, length, self.heads, self.head_dim).transpose(0, 2, 1, 3)
70
77
  k = k.reshape(batch, length, self.heads, self.head_dim).transpose(0, 2, 1, 3)
@@ -80,10 +87,12 @@ class JaxTinyTransformer(TinyTransformer):
80
87
  context = context.transpose(0, 2, 1, 3).reshape(batch, length, self.d_model)
81
88
  attention = context @ params[f"{prefix}.out_weight"] + params[f"{prefix}.out_bias"]
82
89
  residual = hidden + attention
83
- ff_pre = residual @ params[f"{prefix}.ff1_weight"] + params[f"{prefix}.ff1_bias"]
90
+ normed2 = layer_norm(residual, params[f"{prefix}.ln2_gain"], params[f"{prefix}.ln2_bias"])
91
+ ff_pre = normed2 @ params[f"{prefix}.ff1_weight"] + params[f"{prefix}.ff1_bias"]
84
92
  ff_hidden = jnp.maximum(ff_pre, 0)
85
93
  ff = ff_hidden @ params[f"{prefix}.ff2_weight"] + params[f"{prefix}.ff2_bias"]
86
94
  hidden = residual + ff
95
+ hidden = layer_norm(hidden, params["ln_f_gain"], params["ln_f_bias"])
87
96
  if self.task == "text-generation":
88
97
  return hidden @ params["tok_emb"].T + params["head_bias"]
89
98
  mask = jnp.ones((batch, length), dtype=jnp.float32) if attention_mask is None else jnp.asarray(attention_mask)
@@ -49,13 +49,20 @@ class MlxTinyTransformer(TinyTransformer):
49
49
 
50
50
  def _forward_mlx(self, params, input_ids, attention_mask=None):
51
51
  mx = _mlx()
52
+
53
+ def layer_norm(x, gain, bias, eps=1e-5):
54
+ mean = mx.mean(x, axis=-1, keepdims=True)
55
+ var = mx.var(x, axis=-1, keepdims=True)
56
+ return (x - mean) / mx.sqrt(var + eps) * gain + bias
57
+
52
58
  input_ids = mx.array(input_ids, dtype=mx.int32)
53
59
  batch, length = input_ids.shape
54
60
  hidden = params["tok_emb"][input_ids] + params["pos_emb"][mx.arange(length)]
55
61
  causal = mx.tril(mx.ones((length, length), dtype=mx.bool_))
56
62
  for index in range(len(self.blocks)):
57
63
  prefix = f"blocks.{index}"
58
- qkv = hidden @ params[f"{prefix}.qkv_weight"] + params[f"{prefix}.qkv_bias"]
64
+ normed1 = layer_norm(hidden, params[f"{prefix}.ln1_gain"], params[f"{prefix}.ln1_bias"])
65
+ qkv = normed1 @ params[f"{prefix}.qkv_weight"] + params[f"{prefix}.qkv_bias"]
59
66
  q, k, v = mx.split(qkv, 3, axis=-1)
60
67
  q = q.reshape(batch, length, self.heads, self.head_dim).transpose(0, 2, 1, 3)
61
68
  k = k.reshape(batch, length, self.heads, self.head_dim).transpose(0, 2, 1, 3)
@@ -70,10 +77,12 @@ class MlxTinyTransformer(TinyTransformer):
70
77
  context = context.transpose(0, 2, 1, 3).reshape(batch, length, self.d_model)
71
78
  attention = context @ params[f"{prefix}.out_weight"] + params[f"{prefix}.out_bias"]
72
79
  residual = hidden + attention
73
- ff_pre = residual @ params[f"{prefix}.ff1_weight"] + params[f"{prefix}.ff1_bias"]
80
+ normed2 = layer_norm(residual, params[f"{prefix}.ln2_gain"], params[f"{prefix}.ln2_bias"])
81
+ ff_pre = normed2 @ params[f"{prefix}.ff1_weight"] + params[f"{prefix}.ff1_bias"]
74
82
  ff_hidden = mx.maximum(ff_pre, 0)
75
83
  ff = ff_hidden @ params[f"{prefix}.ff2_weight"] + params[f"{prefix}.ff2_bias"]
76
84
  hidden = residual + ff
85
+ hidden = layer_norm(hidden, params["ln_f_gain"], params["ln_f_bias"])
77
86
  if self.task == "text-generation":
78
87
  return hidden @ params["tok_emb"].T + params["head_bias"]
79
88
  mask = mx.ones((batch, length)) if attention_mask is None else mx.array(attention_mask)
@@ -83,6 +83,39 @@ class Module:
83
83
  return self.train(False)
84
84
 
85
85
 
86
+ def layer_norm_forward(x: np.ndarray, gain: np.ndarray, bias: np.ndarray, eps: float = 1e-5):
87
+ """Normalize over the last axis, then apply a learned scale/shift.
88
+
89
+ Returns the normalized output plus a cache used by `layer_norm_backward`.
90
+ """
91
+ mean = x.mean(axis=-1, keepdims=True)
92
+ var = x.var(axis=-1, keepdims=True)
93
+ inv_std = 1.0 / np.sqrt(var + eps)
94
+ x_hat = (x - mean) * inv_std
95
+ out = x_hat * gain + bias
96
+ return out.astype(np.float32), (x_hat, inv_std, gain)
97
+
98
+
99
+ def layer_norm_backward(grad_out: np.ndarray, cache):
100
+ """Backward pass matching `layer_norm_forward`.
101
+
102
+ Returns (grad_x, grad_gain, grad_bias).
103
+ """
104
+ x_hat, inv_std, gain = cache
105
+ d = grad_out.shape[-1]
106
+ flat_grad_out = grad_out.reshape(-1, d)
107
+ flat_x_hat = x_hat.reshape(-1, d)
108
+ grad_gain = (flat_grad_out * flat_x_hat).sum(axis=0)
109
+ grad_bias = flat_grad_out.sum(axis=0)
110
+ grad_x_hat = grad_out * gain
111
+ grad_x = inv_std / d * (
112
+ d * grad_x_hat
113
+ - grad_x_hat.sum(axis=-1, keepdims=True)
114
+ - x_hat * (grad_x_hat * x_hat).sum(axis=-1, keepdims=True)
115
+ )
116
+ return grad_x.astype(np.float32), grad_gain.astype(np.float32), grad_bias.astype(np.float32)
117
+
118
+
86
119
  def softmax_cross_entropy(logits: np.ndarray, targets: np.ndarray, ignore_index=None):
87
120
  flat = logits.reshape(-1, logits.shape[-1]).astype(np.float64)
88
121
  target = targets.reshape(-1).astype(np.int64)
@@ -188,18 +221,23 @@ class LambdaScheduler:
188
221
  self.total_steps = total_steps
189
222
  self.step_count = 0
190
223
  self.base_lr = optimizer.lr
224
+ self.optimizer.lr = self.base_lr * self._factor(0)
225
+
226
+ def _factor(self, step):
227
+ if self.warmup_steps and step < self.warmup_steps:
228
+ return (step + 1) / self.warmup_steps
229
+ return max(
230
+ 0.1,
231
+ 1.0 - (step - self.warmup_steps) / max(1, self.total_steps - self.warmup_steps),
232
+ )
191
233
 
192
234
  def step(self):
193
235
  self.step_count += 1
194
- step = self.step_count - 1
195
- if self.warmup_steps and step < self.warmup_steps:
196
- factor = (step + 1) / self.warmup_steps
197
- else:
198
- factor = max(0.1, 1.0 - (step - self.warmup_steps) / max(1, self.total_steps - self.warmup_steps))
199
- self.optimizer.lr = self.base_lr * factor
236
+ self.optimizer.lr = self.base_lr * self._factor(self.step_count)
200
237
 
201
238
  def state_dict(self):
202
239
  return {"step_count": self.step_count}
203
240
 
204
241
  def load_state_dict(self, state):
205
- self.step_count = int(state.get("step_count", 0))
242
+ self.step_count = int(state.get("step_count", 0))
243
+ self.optimizer.lr = self.base_lr * self._factor(self.step_count)
@@ -1,11 +1,19 @@
1
- """Compact NumPy text model retaining Tensorless transformer behavior."""
1
+ """Compact NumPy text model retaining Tensorless transformer behavior.
2
+
3
+ Architecture: a standard pre-norm decoder-only transformer -- LayerNorm
4
+ before attention and before the feed-forward block, plus a final LayerNorm
5
+ before the output head. Normalization is what keeps deep stacks of residual
6
+ blocks trainable; without it, gradients and activations drift as layers are
7
+ stacked and generation quality suffers noticeably once you go beyond one or
8
+ two blocks.
9
+ """
2
10
 
3
11
  from __future__ import annotations
4
12
 
5
13
  from typing import Optional
6
14
  import numpy as np
7
15
 
8
- from ..engine import Module, Parameter, softmax_cross_entropy
16
+ from ..engine import Module, Parameter, softmax_cross_entropy, layer_norm_forward, layer_norm_backward
9
17
 
10
18
 
11
19
  def _dropout(value, probability, training):
@@ -20,10 +28,14 @@ class TransformerBlock(Module):
20
28
  if d_model % heads:
21
29
  raise ValueError(f"d_model ({d_model}) must be divisible by heads ({heads})")
22
30
  self.heads, self.head_dim, self.dropout = heads, d_model // heads, dropout
31
+ self.ln1_gain = Parameter(np.ones(d_model, dtype=np.float32))
32
+ self.ln1_bias = Parameter(np.zeros(d_model, dtype=np.float32))
23
33
  self.qkv_weight = Parameter(np.random.normal(0, .02, (d_model, 3 * d_model)).astype(np.float32))
24
34
  self.qkv_bias = Parameter(np.zeros(3 * d_model, dtype=np.float32))
25
35
  self.out_weight = Parameter(np.random.normal(0, .02, (d_model, d_model)).astype(np.float32))
26
36
  self.out_bias = Parameter(np.zeros(d_model, dtype=np.float32))
37
+ self.ln2_gain = Parameter(np.ones(d_model, dtype=np.float32))
38
+ self.ln2_bias = Parameter(np.zeros(d_model, dtype=np.float32))
27
39
  hidden = d_model * ff_mult
28
40
  self.ff1_weight = Parameter(np.random.normal(0, .02, (d_model, hidden)).astype(np.float32))
29
41
  self.ff1_bias = Parameter(np.zeros(hidden, dtype=np.float32))
@@ -32,7 +44,8 @@ class TransformerBlock(Module):
32
44
 
33
45
  def forward(self, inputs, attention_mask=None, cache=False):
34
46
  batch, length, d_model = inputs.shape
35
- qkv = inputs @ self.qkv_weight.data + self.qkv_bias.data
47
+ normed1, ln1_cache = layer_norm_forward(inputs, self.ln1_gain.data, self.ln1_bias.data)
48
+ qkv = normed1 @ self.qkv_weight.data + self.qkv_bias.data
36
49
  q, k, v = np.split(qkv, 3, axis=-1)
37
50
  q, k, v = (value.reshape(batch, length, self.heads, self.head_dim).transpose(0, 2, 1, 3)
38
51
  for value in (q, k, v))
@@ -50,26 +63,31 @@ class TransformerBlock(Module):
50
63
  context @ self.out_weight.data + self.out_bias.data, self.dropout, self.training
51
64
  )
52
65
  residual = inputs + attention
53
- ff_pre = residual @ self.ff1_weight.data + self.ff1_bias.data
66
+ normed2, ln2_cache = layer_norm_forward(residual, self.ln2_gain.data, self.ln2_bias.data)
67
+ ff_pre = normed2 @ self.ff1_weight.data + self.ff1_bias.data
54
68
  ff_hidden = np.maximum(ff_pre, 0)
55
69
  ff, ff_keep = _dropout(ff_hidden @ self.ff2_weight.data + self.ff2_bias.data, self.dropout, self.training)
56
70
  output = residual + ff
57
71
  if not cache:
58
72
  return output
59
- return output, (inputs, q, k, v, probabilities, dropped_attention, attention_keep, context,
60
- attention_keep_output, residual, ff_pre, ff_hidden, ff_keep)
73
+ return output, (inputs, normed1, ln1_cache, q, k, v, probabilities, dropped_attention, attention_keep,
74
+ context, attention_keep_output, residual, normed2, ln2_cache, ff_pre, ff_hidden, ff_keep)
61
75
 
62
76
  def backward(self, gradient, cache):
63
- (inputs, q, k, v, probabilities, dropped_attention, attention_keep, context,
64
- attention_keep_output, residual, ff_pre, ff_hidden, ff_keep) = cache
77
+ (inputs, normed1, ln1_cache, q, k, v, probabilities, dropped_attention, attention_keep, context,
78
+ attention_keep_output, residual, normed2, ln2_cache, ff_pre, ff_hidden, ff_keep) = cache
65
79
  gradient_ff = gradient if ff_keep is None else gradient * ff_keep / (1.0 - self.dropout)
66
80
  self.ff2_weight.grad[...] = ff_hidden.reshape(-1, ff_hidden.shape[-1]).T @ gradient_ff.reshape(-1, gradient_ff.shape[-1])
67
81
  self.ff2_bias.grad[...] = gradient_ff.sum(axis=(0, 1))
68
82
  gradient_hidden = gradient_ff @ self.ff2_weight.data.T
69
83
  gradient_hidden *= ff_pre > 0
70
- self.ff1_weight.grad[...] = residual.reshape(-1, residual.shape[-1]).T @ gradient_hidden.reshape(-1, gradient_hidden.shape[-1])
84
+ self.ff1_weight.grad[...] = normed2.reshape(-1, normed2.shape[-1]).T @ gradient_hidden.reshape(-1, gradient_hidden.shape[-1])
71
85
  self.ff1_bias.grad[...] = gradient_hidden.sum(axis=(0, 1))
72
- gradient_residual = gradient + gradient_hidden @ self.ff1_weight.data.T
86
+ gradient_normed2 = gradient_hidden @ self.ff1_weight.data.T
87
+ gradient_residual_from_ff, ln2_gain_grad, ln2_bias_grad = layer_norm_backward(gradient_normed2, ln2_cache)
88
+ self.ln2_gain.grad[...] = ln2_gain_grad
89
+ self.ln2_bias.grad[...] = ln2_bias_grad
90
+ gradient_residual = gradient + gradient_residual_from_ff
73
91
  gradient_attention = gradient_residual if attention_keep_output is None else gradient_residual * attention_keep_output / (1.0 - self.dropout)
74
92
  gradient_context = gradient_attention @ self.out_weight.data.T
75
93
  self.out_weight.grad[...] = context.reshape(-1, context.shape[-1]).T @ gradient_attention.reshape(-1, gradient_attention.shape[-1])
@@ -83,10 +101,28 @@ class TransformerBlock(Module):
83
101
  scale = 1.0 / np.sqrt(self.head_dim)
84
102
  gradient_q = gradient_scores @ k * scale
85
103
  gradient_k = gradient_scores.transpose(0, 1, 3, 2) @ q * scale
86
- gradient_qkv = np.concatenate((gradient_q, gradient_k, gradient_v), axis=-1).transpose(0, 2, 1, 3).reshape(inputs.shape[0], inputs.shape[1], -1)
87
- self.qkv_weight.grad[...] = inputs.reshape(-1, inputs.shape[-1]).T @ gradient_qkv.reshape(-1, gradient_qkv.shape[-1])
104
+ # Merge each of q/k/v back from (batch, heads, length, head_dim) to
105
+ # (batch, length, d_model) *before* concatenating them, matching the
106
+ # forward pass's layout ([q_block | k_block | v_block] along the last
107
+ # axis, each block itself head-major). Concatenating on the head-dim
108
+ # axis first (as a prior version of this code did) interleaves heads
109
+ # and q/k/v in the wrong order and silently corrupts the qkv_weight /
110
+ # qkv_bias gradients for any heads > 1 configuration.
111
+ batch, length = inputs.shape[0], inputs.shape[1]
112
+
113
+ def _merge_heads(gradient):
114
+ return gradient.transpose(0, 2, 1, 3).reshape(batch, length, -1)
115
+
116
+ gradient_qkv = np.concatenate(
117
+ (_merge_heads(gradient_q), _merge_heads(gradient_k), _merge_heads(gradient_v)), axis=-1
118
+ )
119
+ self.qkv_weight.grad[...] = normed1.reshape(-1, normed1.shape[-1]).T @ gradient_qkv.reshape(-1, gradient_qkv.shape[-1])
88
120
  self.qkv_bias.grad[...] = gradient_qkv.sum(axis=(0, 1))
89
- return gradient_residual + gradient_qkv @ self.qkv_weight.data.T
121
+ gradient_normed1 = gradient_qkv @ self.qkv_weight.data.T
122
+ gradient_inputs_from_attn, ln1_gain_grad, ln1_bias_grad = layer_norm_backward(gradient_normed1, ln1_cache)
123
+ self.ln1_gain.grad[...] = ln1_gain_grad
124
+ self.ln1_bias.grad[...] = ln1_bias_grad
125
+ return gradient_residual + gradient_inputs_from_attn
90
126
 
91
127
 
92
128
  class TinyTransformer(Module):
@@ -100,6 +136,8 @@ class TinyTransformer(Module):
100
136
  self.tok_emb = Parameter(np.random.normal(0, .02, (vocab_size, d_model)).astype(np.float32))
101
137
  self.pos_emb = Parameter(np.random.normal(0, .02, (max_seq_len, d_model)).astype(np.float32))
102
138
  self.blocks = [TransformerBlock(d_model, heads, ff_mult, dropout) for _ in range(layers)]
139
+ self.ln_f_gain = Parameter(np.ones(d_model, dtype=np.float32))
140
+ self.ln_f_bias = Parameter(np.zeros(d_model, dtype=np.float32))
103
141
  if task == "text-generation":
104
142
  self.head_bias = Parameter(np.zeros(vocab_size, dtype=np.float32))
105
143
  self.head_weight = self.tok_emb
@@ -127,31 +165,35 @@ class TinyTransformer(Module):
127
165
  caches.append(block_cache)
128
166
  else:
129
167
  hidden = block.forward(hidden, attention_mask)
168
+ hidden_normed, lnf_cache = layer_norm_forward(hidden, self.ln_f_gain.data, self.ln_f_bias.data)
130
169
  if self.task == "text-generation":
131
- out = hidden @ self.tok_emb.data.T + self.head_bias.data
170
+ out = hidden_normed @ self.tok_emb.data.T + self.head_bias.data
132
171
  else:
133
172
  mask = np.ones((batch, length), dtype=np.float32) if attention_mask is None else attention_mask
134
- pooled = (hidden * mask[:, :, None]).sum(1) / np.maximum(mask.sum(1, keepdims=True), 1)
173
+ pooled = (hidden_normed * mask[:, :, None]).sum(1) / np.maximum(mask.sum(1, keepdims=True), 1)
135
174
  out = pooled @ self.head_weight.data + self.head_bias.data
136
- return (out, (input_ids, hidden, attention_mask, caches)) if cache else out
175
+ return (out, (input_ids, hidden, hidden_normed, lnf_cache, attention_mask, caches)) if cache else out
137
176
 
138
177
  def loss_and_backward(self, input_ids, target, attention_mask=None):
139
- logits, (ids, hidden, mask, caches) = self.forward(input_ids, attention_mask, cache=True)
178
+ logits, (ids, hidden, hidden_normed, lnf_cache, mask, caches) = self.forward(input_ids, attention_mask, cache=True)
140
179
  loss, grad = softmax_cross_entropy(logits, target, self.pad_id if self.task == "text-generation" else None)
141
180
  for parameter in self.parameters():
142
181
  parameter.grad.fill(0)
143
182
  self.head_bias.grad[...] = grad.reshape(-1, grad.shape[-1]).sum(axis=0)
144
183
  if self.task == "text-generation":
145
- flat_h, flat_g = hidden.reshape(-1, hidden.shape[-1]), grad.reshape(-1, grad.shape[-1])
146
- self.tok_emb.grad[...] += flat_g.T @ flat_h
147
- dh = (flat_g @ self.tok_emb.data).reshape(hidden.shape)
184
+ flat_hn, flat_g = hidden_normed.reshape(-1, hidden_normed.shape[-1]), grad.reshape(-1, grad.shape[-1])
185
+ self.tok_emb.grad[...] += flat_g.T @ flat_hn
186
+ d_hidden_normed = (flat_g @ self.tok_emb.data).reshape(hidden_normed.shape)
148
187
  else:
149
188
  valid_mask = np.ones(ids.shape, dtype=np.float32) if mask is None else mask
150
- pooled = (hidden * valid_mask[:, :, None]).sum(1) / np.maximum(valid_mask.sum(1, keepdims=True), 1)
189
+ pooled = (hidden_normed * valid_mask[:, :, None]).sum(1) / np.maximum(valid_mask.sum(1, keepdims=True), 1)
151
190
  self.head_weight.grad[...] = pooled.T @ grad
152
191
  self.head_bias.grad[...] = grad.sum(0)
153
- dh = (grad @ self.head_weight.data.T)[:, None, :] * valid_mask[:, :, None]
154
- dh /= np.maximum(valid_mask.sum(1, keepdims=True)[:, :, None], 1)
192
+ d_hidden_normed = (grad @ self.head_weight.data.T)[:, None, :] * valid_mask[:, :, None]
193
+ d_hidden_normed /= np.maximum(valid_mask.sum(1, keepdims=True)[:, :, None], 1)
194
+ dh, lnf_gain_grad, lnf_bias_grad = layer_norm_backward(d_hidden_normed, lnf_cache)
195
+ self.ln_f_gain.grad[...] = lnf_gain_grad
196
+ self.ln_f_bias.grad[...] = lnf_bias_grad
155
197
  for block, block_cache in zip(self.blocks[::-1], caches[::-1]):
156
198
  if self.gradient_checkpointing:
157
199
  block_input, block_mask, rng_state = block_cache
@@ -178,176 +220,3 @@ class TinyTransformer(Module):
178
220
  ids = np.concatenate([ids, next_ids], axis=1)
179
221
  if eos_id is not None and np.all(next_ids == eos_id): break
180
222
  return ids
181
- """A small, dependency-free legacy implementation retained only as inert text.
182
-
183
- Used for:
184
- - "text-generation": next-token prediction over the char vocabulary
185
- - "text-classification": same backbone, with a classification head on
186
- the final token's hidden state instead of a language-modeling head
187
-
188
- Kept intentionally compact -- this is not meant to compete with
189
- production LLM training frameworks, it's meant to give Tensorless a real,
190
- working, from-scratch model that trains fast enough on CPU for the
191
- "zero setup" experience to actually be pleasant.
192
-
193
- from __future__ import annotations
194
-
195
- import math
196
- from typing import Optional
197
-
198
- import numpy as np
199
-
200
-
201
- class CausalSelfAttention(nn.Module):
202
- def __init__(self, d_model: int, heads: int, dropout: float):
203
- super().__init__()
204
- assert d_model % heads == 0, "d_model must be divisible by heads"
205
- self.heads = heads
206
- self.head_dim = d_model // heads
207
- self.qkv = nn.Linear(d_model, 3 * d_model)
208
- self.proj = nn.Linear(d_model, d_model)
209
- self.dropout = dropout
210
- self.resid_drop = nn.Dropout(dropout)
211
-
212
- def forward(self, x: np.ndarray, attn_mask: Optional[np.ndarray] = None) -> np.ndarray:
213
- B, T, C = x.shape
214
- qkv = self.qkv(x)
215
- q, k, v = qkv.split(C, dim=2)
216
- q = q.view(B, T, self.heads, self.head_dim).transpose(1, 2)
217
- k = k.view(B, T, self.heads, self.head_dim).transpose(1, 2)
218
- v = v.view(B, T, self.heads, self.head_dim).transpose(1, 2)
219
-
220
- out = F.scaled_dot_product_attention(
221
- q, k, v, attn_mask=attn_mask, dropout_p=self.dropout if self.training else 0.0, is_causal=attn_mask is None
222
- )
223
- out = out.transpose(1, 2).contiguous().view(B, T, C)
224
- return self.resid_drop(self.proj(out))
225
-
226
-
227
- class MLP(nn.Module):
228
- def __init__(self, d_model: int, ff_mult: int, dropout: float):
229
- super().__init__()
230
- self.fc1 = nn.Linear(d_model, d_model * ff_mult)
231
- self.fc2 = nn.Linear(d_model * ff_mult, d_model)
232
- self.drop = nn.Dropout(dropout)
233
-
234
- def forward(self, x: np.ndarray) -> np.ndarray:
235
- return self.drop(self.fc2(F.gelu(self.fc1(x))))
236
-
237
-
238
- class Block(nn.Module):
239
- def __init__(self, d_model: int, heads: int, ff_mult: int, dropout: float):
240
- super().__init__()
241
- self.ln1 = nn.LayerNorm(d_model)
242
- self.attn = CausalSelfAttention(d_model, heads, dropout)
243
- self.ln2 = nn.LayerNorm(d_model)
244
- self.mlp = MLP(d_model, ff_mult, dropout)
245
-
246
- def forward(self, x: np.ndarray, attn_mask: Optional[np.ndarray] = None) -> np.ndarray:
247
- x = x + self.attn(self.ln1(x), attn_mask=attn_mask)
248
- x = x + self.mlp(self.ln2(x))
249
- return x
250
-
251
-
252
- class TinyTransformer(nn.Module):
253
- Legacy decoder-only transformer usable for LM or sequence classification.
254
-
255
- def __init__(
256
- self,
257
- vocab_size: int,
258
- d_model: int,
259
- layers: int,
260
- heads: int,
261
- ff_mult: int,
262
- dropout: float,
263
- max_seq_len: int,
264
- task: str = "text-generation",
265
- n_classes: int = 0,
266
- pad_id: int = 0,
267
- ):
268
- super().__init__()
269
- self.task = task
270
- self.max_seq_len = max_seq_len
271
- self.pad_id = pad_id
272
-
273
- self.tok_emb = nn.Embedding(vocab_size, d_model)
274
- self.pos_emb = nn.Embedding(max_seq_len, d_model)
275
- self.drop = nn.Dropout(dropout)
276
- self.blocks = nn.ModuleList(
277
- [Block(d_model, heads, ff_mult, dropout) for _ in range(layers)]
278
- )
279
- self.ln_f = nn.LayerNorm(d_model)
280
-
281
- if task == "text-generation":
282
- self.head = nn.Linear(d_model, vocab_size, bias=False)
283
- self.head.weight = self.tok_emb.weight # weight tying
284
- elif task == "text-classification":
285
- assert n_classes > 0, "n_classes must be set for text-classification"
286
- self.head = nn.Linear(d_model, n_classes)
287
- else:
288
- raise ValueError(f"Unsupported task for TinyTransformer: {task}")
289
-
290
- self.apply(self._init_weights)
291
-
292
- def _init_weights(self, module: nn.Module) -> None:
293
- if isinstance(module, nn.Linear):
294
- nn.init.normal_(module.weight, mean=0.0, std=0.02)
295
- if module.bias is not None:
296
- nn.init.zeros_(module.bias)
297
- elif isinstance(module, nn.Embedding):
298
- nn.init.normal_(module.weight, mean=0.0, std=0.02)
299
-
300
- def forward(self, input_ids: torch.Tensor, attention_mask: Optional[torch.Tensor] = None) -> torch.Tensor:
301
- B, T = input_ids.shape
302
- assert T <= self.max_seq_len, (
303
- f"Sequence length {T} exceeds max_seq_len {self.max_seq_len}"
304
- )
305
- pos = torch.arange(T, device=input_ids.device).unsqueeze(0)
306
- x = self.tok_emb(input_ids) + self.pos_emb(pos)
307
- x = self.drop(x)
308
-
309
- attn_mask = None
310
- if attention_mask is not None:
311
- # Combine causal mask with padding mask.
312
- causal = torch.tril(torch.ones(T, T, device=input_ids.device, dtype=torch.bool))
313
- pad = attention_mask.bool().unsqueeze(1).unsqueeze(1) # B,1,1,T
314
- attn_mask = (causal.unsqueeze(0).unsqueeze(0) & pad)
315
-
316
- for block in self.blocks:
317
- x = block(x, attn_mask=attn_mask)
318
- x = self.ln_f(x)
319
-
320
- if self.task == "text-generation":
321
- return self.head(x) # B, T, vocab_size
322
- else:
323
- if attention_mask is not None:
324
- lengths = attention_mask.sum(dim=1).clamp(min=1) - 1
325
- else:
326
- lengths = torch.full((B,), T - 1, device=input_ids.device)
327
- pooled = x[torch.arange(B, device=input_ids.device), lengths]
328
- return self.head(pooled) # B, n_classes
329
-
330
- @torch.no_grad()
331
- def generate(
332
- self,
333
- input_ids: np.ndarray,
334
- max_new_tokens: int,
335
- temperature: float = 0.8,
336
- top_k: Optional[int] = 40,
337
- eos_id: Optional[int] = None,
338
- ) -> np.ndarray:
339
- self.eval()
340
- for _ in range(max_new_tokens):
341
- cond = input_ids[:, -self.max_seq_len:]
342
- logits = self(cond)
343
- logits = logits[:, -1, :] / max(temperature, 1e-5)
344
- if top_k is not None:
345
- v, _ = torch.topk(logits, min(top_k, logits.size(-1)))
346
- logits[logits < v[:, [-1]]] = -float("inf")
347
- probs = F.softmax(logits, dim=-1)
348
- next_id = torch.multinomial(probs, num_samples=1)
349
- input_ids = torch.cat([input_ids, next_id], dim=1)
350
- if eos_id is not None and (next_id == eos_id).all():
351
- break
352
- return input_ids
353
- """