cogforge-engine 2.0.1__tar.gz → 2.1.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.
- cogforge_engine-2.1.0/PKG-INFO +527 -0
- cogforge_engine-2.1.0/README.md +512 -0
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/TODO +3 -1
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/cogforge/app.py +112 -93
- cogforge_engine-2.1.0/cogforge/utils.py +41 -0
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/pyproject.toml +2 -1
- cogforge_engine-2.0.1/PKG-INFO +0 -333
- cogforge_engine-2.0.1/README.md +0 -319
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/.github/workflows/release.yml +0 -0
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/.gitignore +0 -0
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/LICENSE +0 -0
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/cogforge/__init__.py +0 -0
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/cogforge/backend.py +0 -0
- {cogforge_engine-2.0.1 → cogforge_engine-2.1.0}/cogforge/models.py +0 -0
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cogforge-engine
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: A custom autograd engine and Transformer block built from scratch.
|
|
5
|
+
Project-URL: Homepage, https://github.com/avikmjd2/cogforge
|
|
6
|
+
Author-email: Avik Majumder <avikmjd2@gmail.com>
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: graphviz>=0.14.0
|
|
10
|
+
Requires-Dist: numexpr>=2.8.0
|
|
11
|
+
Requires-Dist: numpy>=1.20.0
|
|
12
|
+
Provides-Extra: cuda
|
|
13
|
+
Requires-Dist: cupy>=12.0.0; extra == 'cuda'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# cogforge
|
|
17
|
+
|
|
18
|
+
> A from-scratch deep learning library built on NumPy — a reverse-mode autograd engine extended all the way to working GPTs and encoder–decoder transformers, with optional GPU acceleration.
|
|
19
|
+
|
|
20
|
+
`cogforge` is a small, readable, educational deep learning framework. At its core is a `Tensor` that records every operation into a computation graph and backpropagates through it (micrograd-style), but unlike a toy autograd it scales up to real architectures: MLPs, RNNs, batch/layer normalization, multi-head self- and cross-attention, rotary position embeddings (RoPE), decoder-only GPTs, and a full encoder–decoder Seq2Seq transformer you can actually train and sample from.
|
|
21
|
+
|
|
22
|
+
There is no C++, no PyTorch — just NumPy and explicit, hand-derived gradients. Optionally, the entire backend can be swapped to **CuPy** for GPU execution, or accelerated with **numexpr** on CPU, without changing any model code. The goal is still to *understand* every gradient that flows — speed is a bonus, not the point.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Table of contents
|
|
27
|
+
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Quick start](#quick-start)
|
|
30
|
+
- [Backend: CPU, GPU, numexpr, and no-grad mode](#backend-cpu-gpu-numexpr-and-no-grad-mode)
|
|
31
|
+
- [Core concept: the `Tensor`](#core-concept-the-tensor)
|
|
32
|
+
- [API reference](#api-reference)
|
|
33
|
+
- [Tensor — autograd engine](#tensor--autograd-engine)
|
|
34
|
+
- [Losses](#losses)
|
|
35
|
+
- [Layers](#layers)
|
|
36
|
+
- [Attention](#attention)
|
|
37
|
+
- [Positional encodings](#positional-encodings)
|
|
38
|
+
- [Transformer blocks](#transformer-blocks)
|
|
39
|
+
- [Containers](#containers)
|
|
40
|
+
- [Recurrent](#recurrent)
|
|
41
|
+
- [Optimizers](#optimizers)
|
|
42
|
+
- [Models](#models)
|
|
43
|
+
- [Worked example: train a char-level GPT](#worked-example-train-a-char-level-gpt)
|
|
44
|
+
- [Worked example: encoder–decoder Seq2Seq](#worked-example-encoderdecoder-seq2seq)
|
|
45
|
+
- [Gotchas](#gotchas)
|
|
46
|
+
- [Roadmap](#roadmap)
|
|
47
|
+
- [License](#license)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install cogforge-engine
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python 3.8+ and NumPy — that's the only hard dependency. Two optional extras unlock acceleration:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install cupy-cuda12x # GPU backend (pick the build matching your CUDA version)
|
|
61
|
+
pip install numexpr # multi-threaded CPU element-wise ops
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The package is organized into three modules:
|
|
65
|
+
|
|
66
|
+
| Module | Contains |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `cogforge.backend` | The swappable array backend: NumPy ↔ CuPy switching, numexpr flag, global no-grad flag. |
|
|
69
|
+
| `cogforge.app` | The autograd engine (`Tensor`) and every building block — layers, optimizers, losses, normalization, attention, positional encodings. |
|
|
70
|
+
| `cogforge.models` | Ready-to-use models: `GPTV1`, `GPT2`, `Seq2Seq`. |
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from cogforge.app import Tensor, Linear, Adam, MultiHeadAttention # building blocks
|
|
74
|
+
from cogforge.models import GPTV1, GPT2, Seq2Seq # models
|
|
75
|
+
from cogforge import backend # device control
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Quick start
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import numpy as np
|
|
84
|
+
from cogforge.app import Tensor
|
|
85
|
+
|
|
86
|
+
# Build a graph
|
|
87
|
+
a = Tensor(np.array([2.0, 3.0]))
|
|
88
|
+
b = Tensor(np.array([4.0, 5.0]))
|
|
89
|
+
c = (a * b).sigmoid().softmax()
|
|
90
|
+
|
|
91
|
+
# Backpropagate (note the spelling: backwards, with an 's')
|
|
92
|
+
c.backwards()
|
|
93
|
+
|
|
94
|
+
print(a.grad) # gradient of the output w.r.t. a
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Every `Tensor` carries a `.data` (the backend array), a `.grad` (same shape, accumulates gradients), and a hidden `_backwards` closure that knows how to push gradient to its parents. Calling `.backwards()` on any node runs a topological sort and walks the graph in reverse.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Backend: CPU, GPU, numexpr, and no-grad mode
|
|
102
|
+
|
|
103
|
+
`cogforge.backend` exposes a module-level `np` that every layer and model routes through. By default it *is* NumPy; flipping one switch reroutes the whole library to CuPy.
|
|
104
|
+
|
|
105
|
+
### GPU (CuPy)
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from cogforge import backend
|
|
109
|
+
backend.use_gpu(True) # everything created after this lives on the GPU
|
|
110
|
+
# ... build model, train ...
|
|
111
|
+
backend.use_gpu(False) # back to NumPy
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- Raises `RuntimeError` if CuPy is not installed.
|
|
115
|
+
- Switch **before** constructing your model — parameters are allocated on whichever device is active at creation time.
|
|
116
|
+
- `Embedding`'s scatter-add backward automatically uses `cupyx.scatter_add` on GPU and `np.add.at` on CPU.
|
|
117
|
+
- Sampling in `generate()` always happens on CPU (logits are pulled back with `to_cpu`), so generation works identically on either device.
|
|
118
|
+
|
|
119
|
+
### numexpr (CPU acceleration)
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from cogforge.app import set_numexpr
|
|
123
|
+
set_numexpr(True, threads=8) # multi-threaded softmax / fused elementwise ops
|
|
124
|
+
set_numexpr(False)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Only takes effect on the CPU backend (ignored when the GPU is active). Raises if numexpr isn't installed.
|
|
128
|
+
|
|
129
|
+
### No-grad mode
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from cogforge.app import needGradientHence
|
|
133
|
+
needGradientHence(False) # stop building graphs: no .grad buffers, no closures
|
|
134
|
+
# ... fast inference ...
|
|
135
|
+
needGradientHence(True) # back to training mode
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
When gradients are off, every op returns a bare result tensor — no children, no backward closure, no gradient buffers — which slashes memory use and speeds up inference. All three models' `generate()` methods toggle this automatically and restore the previous state afterwards (in a `try/finally`, so it's restored even on error).
|
|
139
|
+
|
|
140
|
+
### Helpers
|
|
141
|
+
|
|
142
|
+
| Function | Purpose |
|
|
143
|
+
| --- | --- |
|
|
144
|
+
| `to_cpu(a)` | Return a NumPy array regardless of the active backend. Use it before plotting, sampling, or saving. |
|
|
145
|
+
| `scatter_add(target, indices, values)` | Backend-aware `target[indices] += values` (handles repeated indices correctly on both devices). |
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Core concept: the `Tensor`
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
Tensor(array, children=(), requires_grad=True, typed="compressed")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
| Argument | Meaning |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| `array` | Any array-like; stored on the active backend in `.data`. |
|
|
158
|
+
| `children` | Parent tensors in the graph (set internally by ops; you rarely pass this). |
|
|
159
|
+
| `requires_grad` | Reserved flag (currently informational). |
|
|
160
|
+
| `typed` | `"compressed"` → `float32` (default), anything else → `float64`. |
|
|
161
|
+
|
|
162
|
+
Gradients **accumulate** into `.grad`. Always zero them between optimization steps (the optimizers do this for you via `zero_grad()`). When global no-grad mode is on, `.grad` is `None` and no graph is recorded.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## API reference
|
|
167
|
+
|
|
168
|
+
### Tensor — autograd engine
|
|
169
|
+
|
|
170
|
+
**Differentiable operations** (each builds graph and defines its own backward):
|
|
171
|
+
|
|
172
|
+
| Operation | Notes |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `a + b`, `a - b`, `a * b` | Elementwise, with broadcasting support. |
|
|
175
|
+
| `a @ b` | Batched matmul; gradients are correctly un-broadcast. |
|
|
176
|
+
| `a[key]` | Indexing/slicing. |
|
|
177
|
+
| `Tensor.cat(tensors, axis=-1)` | Classmethod. Concatenates a tuple of tensors along `axis`; backward splits the gradient back to each parent. (Used internally by RoPE.) |
|
|
178
|
+
| `.relu()` | |
|
|
179
|
+
| `.sigmoid()` | |
|
|
180
|
+
| `.tanh()` | |
|
|
181
|
+
| `.softmax(axis=-1)` | Numerically stable (max-subtraction); numexpr-accelerated when enabled. |
|
|
182
|
+
| `.view(shape)` | Reshape (handles non-contiguous data). |
|
|
183
|
+
| `.flatten()` | Flattens everything after the batch dim → `(B, -1)`. |
|
|
184
|
+
| `.flatten_consective(num)` | Groups `num` consecutive timesteps. Expects a 3-D `(B, T, C)` tensor; `T` must be divisible by `num`. |
|
|
185
|
+
| `.transpose(axes)` | Permute axes (pass the full permutation tuple). |
|
|
186
|
+
| `.masked_fill(mask, value)` | Sets entries where `mask` is `True` to `value` (used for causal/padding attention masks). |
|
|
187
|
+
| `.dropout(p=0.1, training=True)` | Inverted dropout: scales by `1/(1-p)` at train time, identity when `training=False`. |
|
|
188
|
+
| `.dropTheWholeNeuron(p=0.1, training=True, axis=-1, batch_ind=0)` | Structured dropout — zeroes entire feature channels rather than individual elements. |
|
|
189
|
+
|
|
190
|
+
**Backward pass**
|
|
191
|
+
|
|
192
|
+
| Method | Notes |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| `.backwards()` | **Primary.** Iterative topological sort — safe for deep/long graphs. |
|
|
195
|
+
| `.backwards_recursive()` | Legacy recursive version; can hit Python's recursion limit on long sequences. Prefer `.backwards()`. |
|
|
196
|
+
|
|
197
|
+
**Static helper**
|
|
198
|
+
|
|
199
|
+
- `Tensor.unbroadcast(grad, shape)` — reduces a broadcasted gradient back to the original parameter shape. Used internally.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
### Losses
|
|
204
|
+
|
|
205
|
+
All losses are **classmethods** on `Tensor` and return a scalar loss tensor you call `.backwards()` on. Mind the distinction between losses that take **probabilities** and losses that take **raw logits** — this is the most common mistake.
|
|
206
|
+
|
|
207
|
+
| Loss | Input expectation | Use when |
|
|
208
|
+
| --- | --- | --- |
|
|
209
|
+
| `Tensor.softmax_cross_entropy(scores, targets)` | `scores` are **raw logits**, `targets` one-hot. Softmax fused inside (stable). Works for 2-D `(B,V)` and 3-D `(B,T,V)`. | Standard classification / LM. **Recommended.** |
|
|
210
|
+
| `Tensor.sparse_softmax_cross_entropy(scores, target_ids)` | `scores` raw logits `(B,T,V)`, `target_ids` integers `(B,T)`. | Language modeling — skips building one-hot targets. |
|
|
211
|
+
| `Tensor.sparse_softmax_cross_entropy_index(scores, labels, mask)` | Raw logits + integer labels + per-token mask (1 = real, 0 = pad). | Padded LM / seq2seq batches. **Recommended for Seq2Seq training.** |
|
|
212
|
+
| `Tensor.softmax_cross_entropy_masked(scores, targets, mask)` | Logits + one-hot targets + per-row mask. | Padded batches, fused softmax. |
|
|
213
|
+
| `Tensor.cross_entropy_loss(predictions, targets)` | `predictions` are **probabilities** (call `.softmax()` first), `targets` one-hot. | You already have a softmax in your graph. |
|
|
214
|
+
| `Tensor.cross_entropy_loss_masked(predictions, targets, mask)` | Probabilities + per-row mask. | Padded batches without fused softmax. |
|
|
215
|
+
|
|
216
|
+
> ℹ️ The `softmax_*` and `sparse_softmax_*` variants apply softmax internally — feed them **raw logits**. `cross_entropy_loss*` is the opposite — it expects probabilities.
|
|
217
|
+
>
|
|
218
|
+
> `sparse_softmax_cross_entropy_legacy` and `softmax_cross_entropy_old` are kept for reference; prefer the current versions.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### Layers
|
|
223
|
+
|
|
224
|
+
#### `Linear(nin, nout)`
|
|
225
|
+
Affine transform `x @ W + b`. He-initialized weights. `.parameters()` → `[W, b]`.
|
|
226
|
+
|
|
227
|
+
#### `Embedding(vocab_size, embedding_dim)`
|
|
228
|
+
Lookup table. Call with an integer index array; backward scatters gradients correctly (repeated indices accumulate, on CPU and GPU). `.parameters()` → `[weights]`.
|
|
229
|
+
|
|
230
|
+
#### `LayerNorm(dim, eps=1e-5)`
|
|
231
|
+
Normalizes over the last dimension. Learnable `gamma`/`beta`, full hand-derived backward. `.parameters()` → `[gamma, beta]`.
|
|
232
|
+
|
|
233
|
+
#### `BatchNorm1D(dim, eps=1e-5, momentum=0.1)`
|
|
234
|
+
Normalizes over the batch (and time, for 3-D input). Tracks `running_mean`/`running_var` for inference. Toggle `.training = True/False`. Learnable `gamma`/`beta`.
|
|
235
|
+
|
|
236
|
+
#### `FeedForward(dmodel, dff=None)`
|
|
237
|
+
Position-wise MLP: `Linear → ReLU → Dropout(0.15) → Linear`. `dff` defaults to `4 * dmodel`. Dropout is active only when called with `is_training=True` (transformer blocks handle this for you via their train/infer state).
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
### Attention
|
|
242
|
+
|
|
243
|
+
#### `Attention(dk)`
|
|
244
|
+
Scaled dot-product attention. Call `attention(Q, K, V, mask=None)`. `dk` sets the `1/√dk` scale. Masked positions are filled with `-1e9` before the softmax.
|
|
245
|
+
|
|
246
|
+
#### `MultiHeadAttention(dinp, dmodel, dout, n, rope=None)`
|
|
247
|
+
`n` heads, `dmodel` split into `n` chunks of size `dmodel // n` (must divide evenly). Projects input `dinp → dmodel`, attends, projects `dmodel → dout`. If a `rope` (see [RotatoryPositionalEncoding](#positional-encodings)) is passed, it is applied to Q and K after the head split — this is how `GPT2` gets rotary positions. Call `mha(query, key, value, mask=None)`. `.parameters()` returns all four projection layers' params.
|
|
248
|
+
|
|
249
|
+
#### `CrossAttention(dim_dec, dim_enc, d_out, dec_rope=None, enc_rope=None, d_k=None, h=None, d_model=None)`
|
|
250
|
+
Attention where **queries come from the decoder stream and keys/values from the encoder stream** — the bridge of an encoder–decoder transformer. Specify head geometry as either (`d_k` and `h`) or (`d_model` and `h`). Optional separate RoPE for the query (decoder) side and key (encoder) side. Call `cross(x_decod, x_encod, mask=None)`; pass the encoder padding mask as `mask` so the decoder never attends to pad tokens.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### Positional encodings
|
|
255
|
+
|
|
256
|
+
#### `PositionalEncoding(max_len, dmodel)`
|
|
257
|
+
Fixed sinusoidal positions, **added** to the input embeddings. Call `pe(x)`. No parameters. Used by `GPTV1`.
|
|
258
|
+
|
|
259
|
+
#### `RotatoryPositionalEncoding(max_len, dim, base=10000.0)`
|
|
260
|
+
Rotary position embeddings (RoPE). Instead of adding position vectors to embeddings, it **rotates Q and K inside attention**, encoding *relative* position directly in the dot product. `dim` is the per-head dimension `d_k` (must be even), not `d_model`. Construct once and hand the same instance to every block:
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
rope = RotatoryPositionalEncoding(max_len, d_model // n_heads)
|
|
264
|
+
block = Transformer(dmodel=d_model, n=n_heads, rope=rope)
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
No parameters. Used by `GPT2` and (optionally) `Seq2Seq`.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### Transformer blocks
|
|
272
|
+
|
|
273
|
+
#### `Transformer(dmodel, n, dff=None, rope=None, is_training=False)`
|
|
274
|
+
A **pre-norm** self-attention block: `x + Attn(LN(x))` then `x + FF(LN(x))`. `n` = number of heads. Optional RoPE. Call `block(x, mask=None)` — pass a causal mask for LM use or a padding mask for encoder use.
|
|
275
|
+
|
|
276
|
+
State control: `.train(enabled=True)` / `.infer(enabled=True)` toggle `is_training`, which switches the feed-forward dropout on/off.
|
|
277
|
+
|
|
278
|
+
#### `Decoder(d_model, n_heads, d_ff=None, is_training=False, dec_in_rope=None, enc_rope=None, dec_rope=None)`
|
|
279
|
+
A full **pre-norm encoder–decoder block** with three sublayers:
|
|
280
|
+
|
|
281
|
+
1. masked self-attention over the decoder stream (`dec_in_rope` optional),
|
|
282
|
+
2. cross-attention into the encoder output (`dec_rope` on queries, `enc_rope` on keys, both optional),
|
|
283
|
+
3. feed-forward with dropout.
|
|
284
|
+
|
|
285
|
+
Call `block(x_dec, x_enc, mask=None, cross_mask=None)` — `mask` is the causal mask for self-attention, `cross_mask` the encoder padding mask. Same `.train()` / `.infer()` interface as `Transformer`.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### Containers
|
|
290
|
+
|
|
291
|
+
#### `Sequential(layers)`
|
|
292
|
+
Runs layers in order. `.train()` / `.test()` flip the `training` flag on any layer that has one (e.g. `BatchNorm1D`).
|
|
293
|
+
|
|
294
|
+
> ⚠️ `Sequential.parameters()` only collects layers exposing `W`, `b`, `gamma`, or `beta` attributes (i.e. `Linear`, `LayerNorm`, `BatchNorm1D`). Composite layers like `MultiHeadAttention`, `FeedForward`, and `Transformer` hold sub-modules, so their parameters are **not** picked up here — gather those via each module's own `.parameters()`.
|
|
295
|
+
|
|
296
|
+
#### `MLP(layer_sizes)`
|
|
297
|
+
Convenience feed-forward net: `Linear → ReLU` between layers, plain `Linear` output. Built from a list of sizes, e.g. `MLP([784, 128, 64, 10])`.
|
|
298
|
+
|
|
299
|
+
- `.save(filename="best_model.npz")` / `.load(filename="best_model.npz")` — persist/restore weights.
|
|
300
|
+
- *Note:* `MLP` does not expose a `parameters()` method; collect them via `[p for layer in mlp.layers for p in layer.parameters()]` if you want to optimize it.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### Recurrent
|
|
305
|
+
|
|
306
|
+
#### `RNNCell(input_dim, hidden_dim)`
|
|
307
|
+
One tanh recurrence step: `h_next = tanh(i2h(x) + h2h(h_prev))`. `.parameters()` included.
|
|
308
|
+
|
|
309
|
+
#### `RNN(input_dim, hidden_dim)`
|
|
310
|
+
Unrolls a cell over a **list** of timestep tensors (each `(B, input_dim)`) and returns the list of hidden states (each `(B, hidden_dim)`). Optional `prev_hidden`.
|
|
311
|
+
|
|
312
|
+
#### `StackedRNN(input_dim, hidden_dim, num_layers)`
|
|
313
|
+
Multiple `RNN` layers stacked. Returns `(top_layer_states, per_layer_final_states)` — the second value is convenient for seq2seq.
|
|
314
|
+
|
|
315
|
+
#### `Bridge(enc_hidden, dec_hidden, enc_layers, dec_layers, mode="project")`
|
|
316
|
+
Maps RNN encoder final hidden states to decoder initial hidden states, handling mismatched layer counts and hidden sizes.
|
|
317
|
+
|
|
318
|
+
| `mode` | Behavior |
|
|
319
|
+
| --- | --- |
|
|
320
|
+
| `"project"` | One learned `Linear(enc_hidden → dec_hidden)` per decoder layer. General, recommended. |
|
|
321
|
+
| `"tie"` | No parameters; requires `enc_hidden == dec_hidden`. Selects/repeats raw states. |
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
### Optimizers
|
|
326
|
+
|
|
327
|
+
Both take an iterable of parameter tensors and share the same interface: `step()`, `zero_grad()`, `clip_grads(max_norm=5.0)`.
|
|
328
|
+
|
|
329
|
+
#### `SGD(parameters, learning_rate=0.01)`
|
|
330
|
+
Plain stochastic gradient descent.
|
|
331
|
+
|
|
332
|
+
#### `Adam(parameters, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8)`
|
|
333
|
+
Adam with bias correction. Recommended for transformers.
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
opt = Adam(model.parameters(), lr=3e-4)
|
|
337
|
+
opt.zero_grad()
|
|
338
|
+
loss.backwards()
|
|
339
|
+
opt.clip_grads(1.0) # optional gradient clipping
|
|
340
|
+
opt.step()
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
### Models
|
|
346
|
+
|
|
347
|
+
All three models share conventions:
|
|
348
|
+
|
|
349
|
+
- `model.parameters()` returns every trainable tensor (deduplicated where weights are shared).
|
|
350
|
+
- `model.generate(...)` automatically switches to inference mode and disables gradient tracking for the duration, restoring the previous state afterwards — you never need to toggle anything manually to sample.
|
|
351
|
+
- Sampling supports `temperature` and `top_k`, is numerically stabilized, and always runs on CPU regardless of backend.
|
|
352
|
+
|
|
353
|
+
#### `GPTV1(vocab, d_model, n_heads, n_layers, max_len, d_ff=None)`
|
|
354
|
+
A decoder-only transformer with **sinusoidal (additive) positional encoding**: token embedding + positions + stacked pre-norm `Transformer` blocks + final `LayerNorm` + output head. Causal masking is applied internally.
|
|
355
|
+
|
|
356
|
+
| Method | Description |
|
|
357
|
+
| --- | --- |
|
|
358
|
+
| `model(idx)` | `idx`: integer array `(B, T)`. Returns logits `(B, T, vocab)`. |
|
|
359
|
+
| `model.generate(idx, n_new, temperature=1.0, top_k=None)` | Autoregressive sampling. Crops the context to the last `max_len` tokens. Returns `(B, T + n_new)`. |
|
|
360
|
+
|
|
361
|
+
#### `GPT2(vocab, d_model, n_heads, n_layers, max_len, d_ff=None, base=10000.0, training=False)`
|
|
362
|
+
The modern decoder-only variant: **RoPE instead of additive positions** (one shared `RotatoryPositionalEncoding` of dim `d_model // n_heads` applied to Q/K in every block), no positional add at the input, dropout in the feed-forward layers when training. `base` is the RoPE frequency base.
|
|
363
|
+
|
|
364
|
+
| Method | Description |
|
|
365
|
+
| --- | --- |
|
|
366
|
+
| `model(idx)` | Logits `(B, T, vocab)` with causal masking applied internally. |
|
|
367
|
+
| `model.train()` / `model.infer()` | Toggle training mode (dropout on/off) across all blocks. |
|
|
368
|
+
| `model.generate(idx, n_new, temperature=1.0, top_k=None)` | As `GPTV1`; also handles the train/infer switch for you. |
|
|
369
|
+
|
|
370
|
+
#### `Seq2Seq(enc_vocab, dec_vocab, d_model, n_heads, num_enc_layers, num_dec_layers, max_len, d_ff=None, training=False, shared_tok=False, pad_id=0, encoder_rope=None, dec_in_rope=None, dec_rope=None, enc_rope=None)`
|
|
371
|
+
A full **encoder–decoder transformer** (the original *Attention Is All You Need* topology, pre-norm):
|
|
372
|
+
|
|
373
|
+
- **Encoder:** `num_enc_layers` self-attention `Transformer` blocks over the source, with a padding mask built from `pad_id`, followed by a final encoder `LayerNorm`.
|
|
374
|
+
- **Decoder:** `num_dec_layers` `Decoder` blocks — causal self-attention, cross-attention into the encoder output (respecting the encoder padding mask), feed-forward.
|
|
375
|
+
- **Weight tying:** with `shared_tok=True` and `enc_vocab == dec_vocab`, the encoder embedding, decoder embedding, and output projection all share one matrix (embeddings scaled by `√d_model`, plus a learned output bias). Cuts parameter count substantially.
|
|
376
|
+
- **RoPE, opt-in per site:** pass any non-`None` value to `encoder_rope` (encoder self-attention), `dec_in_rope` (decoder self-attention), `dec_rope` (cross-attention queries), and/or `enc_rope` (cross-attention keys) to enable a shared rotary encoding at that site.
|
|
377
|
+
|
|
378
|
+
| Method | Description |
|
|
379
|
+
| --- | --- |
|
|
380
|
+
| `model(enc_idx, dec_idx)` | Teacher-forced forward. `enc_idx`: source `(B, T_enc)`; `dec_idx`: shifted target starting with `<SOS>`, `(B, T_dec)`. Returns logits `(B, T_dec, dec_vocab)`. |
|
|
381
|
+
| `model.encode(enc_idx)` | Run the encoder once; returns `(x_enc, enc_pad_mask)` for reuse across decode steps. |
|
|
382
|
+
| `model.decode_step(dec_idx, x_enc, enc_pad)` | Decoder forward against a fixed encoder output. |
|
|
383
|
+
| `model.generate(enc_idx, sos_id, eos_id=None, max_new=50, temperature=1.0, top_k=None)` | Encodes once, then autoregressively decodes from `<SOS>`; stops early when every sequence in the batch emits `eos_id`. |
|
|
384
|
+
| `model.train()` / `model.infer()` | Toggle dropout across all encoder and decoder blocks. |
|
|
385
|
+
| `Seq2Seq.make_pad_mask(idx, pad_id)` | Static helper: `(B, T)` ints → `(B, 1, 1, T)` boolean mask, `True` at padding. |
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Worked example: train a char-level GPT
|
|
390
|
+
|
|
391
|
+
Works identically with `GPTV1`; shown with the RoPE-based `GPT2`.
|
|
392
|
+
|
|
393
|
+
```python
|
|
394
|
+
import numpy as np
|
|
395
|
+
from cogforge.app import Tensor, Adam
|
|
396
|
+
from cogforge.models import GPT2
|
|
397
|
+
|
|
398
|
+
# --- data -------------------------------------------------------------
|
|
399
|
+
text = open("input.txt").read()
|
|
400
|
+
chars = sorted(set(text))
|
|
401
|
+
stoi = {c: i for i, c in enumerate(chars)}
|
|
402
|
+
itos = {i: c for i, c in enumerate(chars)}
|
|
403
|
+
data = np.array([stoi[c] for c in text])
|
|
404
|
+
vocab = len(chars)
|
|
405
|
+
|
|
406
|
+
# --- model ------------------------------------------------------------
|
|
407
|
+
block = 64
|
|
408
|
+
model = GPT2(vocab=vocab, d_model=128, n_heads=4,
|
|
409
|
+
n_layers=4, max_len=block, training=True)
|
|
410
|
+
opt = Adam(model.parameters(), lr=3e-4)
|
|
411
|
+
|
|
412
|
+
def get_batch(bs=32):
|
|
413
|
+
ix = np.random.randint(0, len(data) - block - 1, size=bs)
|
|
414
|
+
x = np.stack([data[i:i + block] for i in ix])
|
|
415
|
+
y = np.stack([data[i + 1:i + block + 1] for i in ix])
|
|
416
|
+
return x, y
|
|
417
|
+
|
|
418
|
+
# --- train ------------------------------------------------------------
|
|
419
|
+
for step in range(2000):
|
|
420
|
+
x, y = get_batch()
|
|
421
|
+
logits = model(x) # (B, T, vocab)
|
|
422
|
+
loss = Tensor.sparse_softmax_cross_entropy(logits, y)
|
|
423
|
+
|
|
424
|
+
opt.zero_grad()
|
|
425
|
+
loss.backwards()
|
|
426
|
+
opt.clip_grads(1.0)
|
|
427
|
+
opt.step()
|
|
428
|
+
|
|
429
|
+
if step % 100 == 0:
|
|
430
|
+
print(f"step {step:4d} | loss {float(loss.data):.4f}")
|
|
431
|
+
|
|
432
|
+
# --- sample -----------------------------------------------------------
|
|
433
|
+
ctx = np.array([[stoi["\n"]]])
|
|
434
|
+
out = model.generate(ctx, n_new=300, temperature=0.8, top_k=20)
|
|
435
|
+
print("".join(itos[int(i)] for i in out[0]))
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
To run the same script on GPU, add two lines at the top — before building the model:
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
from cogforge import backend
|
|
442
|
+
backend.use_gpu(True)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## Worked example: encoder–decoder Seq2Seq
|
|
448
|
+
|
|
449
|
+
A toy translation/unscrambling setup with a shared vocabulary, tied weights, and RoPE everywhere:
|
|
450
|
+
|
|
451
|
+
```python
|
|
452
|
+
import numpy as np
|
|
453
|
+
from cogforge.app import Tensor, Adam
|
|
454
|
+
from cogforge.models import Seq2Seq
|
|
455
|
+
|
|
456
|
+
PAD, SOS, EOS = 0, 1, 2
|
|
457
|
+
vocab = 40
|
|
458
|
+
|
|
459
|
+
model = Seq2Seq(
|
|
460
|
+
enc_vocab=vocab, dec_vocab=vocab,
|
|
461
|
+
d_model=128, n_heads=4,
|
|
462
|
+
num_enc_layers=3, num_dec_layers=3,
|
|
463
|
+
max_len=32, training=True,
|
|
464
|
+
shared_tok=True, pad_id=PAD,
|
|
465
|
+
encoder_rope=True, dec_in_rope=True, # any non-None value enables RoPE at that site
|
|
466
|
+
)
|
|
467
|
+
opt = Adam(model.parameters(), lr=3e-4)
|
|
468
|
+
|
|
469
|
+
for step in range(num_steps):
|
|
470
|
+
src, tgt = get_batch() # src: (B, T_enc) padded with PAD
|
|
471
|
+
# tgt: (B, T_dec+1) = [SOS, ..., EOS, PAD...]
|
|
472
|
+
dec_in, labels = tgt[:, :-1], tgt[:, 1:]
|
|
473
|
+
logits = model(src, dec_in) # (B, T_dec, vocab)
|
|
474
|
+
|
|
475
|
+
mask = (labels != PAD).astype(np.float32)
|
|
476
|
+
loss = Tensor.sparse_softmax_cross_entropy_index(logits, labels, mask)
|
|
477
|
+
|
|
478
|
+
opt.zero_grad()
|
|
479
|
+
loss.backwards()
|
|
480
|
+
opt.clip_grads(1.0)
|
|
481
|
+
opt.step()
|
|
482
|
+
|
|
483
|
+
# inference: encode once, decode token by token, stop on EOS
|
|
484
|
+
out = model.generate(src, sos_id=SOS, eos_id=EOS, max_new=32,
|
|
485
|
+
temperature=1.0, top_k=5)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Gotchas
|
|
491
|
+
|
|
492
|
+
- **It's `backwards()`, not `backward()`.** The backward pass method has a trailing `s`.
|
|
493
|
+
- **Logits vs. probabilities.** `softmax_cross_entropy` / `sparse_softmax_cross_entropy*` fuse the softmax internally — feed them **raw logits**. `cross_entropy_loss*` expects **probabilities**. Mixing these up silently trains the wrong thing.
|
|
494
|
+
- **Gradients accumulate.** Call `optimizer.zero_grad()` every step (or `p.grad[...] = 0`), or gradients pile up across iterations.
|
|
495
|
+
- **Switch the backend before building the model.** `use_gpu(True)` after construction leaves your parameters stranded on the CPU while new activations land on the GPU.
|
|
496
|
+
- **RoPE dim is per-head.** `RotatoryPositionalEncoding` takes `d_model // n_heads` (which must be even), not `d_model`. The models handle this internally — it only matters if you wire blocks up yourself.
|
|
497
|
+
- **Training vs. inference mode matters now.** `GPT2` and `Seq2Seq` use dropout; call `.train()` before optimizing and `.infer()` before evaluating. `generate()` handles this (and no-grad mode) for you and restores the prior state afterwards.
|
|
498
|
+
- **`Sequential.parameters()` is shallow** — see the note under [Containers](#containers). For attention/feed-forward/transformer stacks, gather parameters through each module's own `.parameters()` (as the models' `parameters()` methods do).
|
|
499
|
+
- **RNNs operate on lists**, not a single `(B, T, C)` tensor — pass a list of per-timestep tensors.
|
|
500
|
+
- **Don't dedupe tied parameters yourself.** `Seq2Seq.parameters()` already deduplicates shared tensors by identity, so the tied embedding is only updated once per step.
|
|
501
|
+
|
|
502
|
+
---
|
|
503
|
+
|
|
504
|
+
## Roadmap
|
|
505
|
+
|
|
506
|
+
Shipped since the last release:
|
|
507
|
+
|
|
508
|
+
- ✅ RoPE (rotary position embeddings), usable in GPT and at every attention site of the Seq2Seq model
|
|
509
|
+
- ✅ Full encoder–decoder transformer (`Seq2Seq`) with cross-attention and padding masks
|
|
510
|
+
- ✅ Weight tying between embeddings and the output head
|
|
511
|
+
- ✅ GPU backend via CuPy; numexpr-accelerated CPU ops
|
|
512
|
+
- ✅ Dropout (element-wise and structured) with train/infer modes
|
|
513
|
+
- ✅ Global no-grad mode for fast, memory-light inference
|
|
514
|
+
|
|
515
|
+
Planned / under consideration:
|
|
516
|
+
|
|
517
|
+
- KV cache for faster generation
|
|
518
|
+
- SwiGLU feed-forward and RMSNorm
|
|
519
|
+
- RoPE length interpolation
|
|
520
|
+
- Linear-attention block (as a study in the recall-vs-cost tradeoff)
|
|
521
|
+
- Checkpoint save/load for the transformer models
|
|
522
|
+
|
|
523
|
+
---
|
|
524
|
+
|
|
525
|
+
## License
|
|
526
|
+
|
|
527
|
+
MIT License. See [LICENSE](LICENSE) for details.
|