cogforge-engine 2.1.1__tar.gz → 2.1.3__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.1 → cogforge_engine-2.1.3}/.gitignore +5 -1
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/PKG-INFO +146 -27
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/README.md +145 -26
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/TODO +3 -1
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/cogforge/app.py +114 -183
- cogforge_engine-2.1.3/cogforge/legacy.py +179 -0
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/cogforge/models.py +10 -3
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/pyproject.toml +1 -1
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/.github/workflows/release.yml +0 -0
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/LICENSE +0 -0
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/cogforge/__init__.py +0 -0
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/cogforge/backend.py +0 -0
- {cogforge_engine-2.1.1 → cogforge_engine-2.1.3}/cogforge/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cogforge-engine
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.3
|
|
4
4
|
Summary: A custom autograd engine and Transformer block built from scratch.
|
|
5
5
|
Project-URL: Homepage, https://github.com/avikmjd2/cogforge
|
|
6
6
|
Author-email: Avik Majumder <avikmjd2@gmail.com>
|
|
@@ -19,12 +19,15 @@ Description-Content-Type: text/markdown
|
|
|
19
19
|
|
|
20
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
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.
|
|
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. Every tensor also records the operation that produced it, and the built-in **Graphviz visualizer** can render the full computation graph — data, gradients, and ops — so you can literally *see* backpropagation. The goal is still to *understand* every gradient that flows — speed is a bonus, not the point.
|
|
23
|
+
|
|
24
|
+
> **This release overhauls the loss API.** Seven overlapping cross-entropy variants have been unified into three functions covering every use case — with built-in **label smoothing**, optional padding masks everywhere, and one consistent shape/normalization convention. The superseded losses live on in `cogforge.legacy`. See [What's new](#whats-new).
|
|
23
25
|
|
|
24
26
|
---
|
|
25
27
|
|
|
26
28
|
## Table of contents
|
|
27
29
|
|
|
30
|
+
- [What's new](#whats-new)
|
|
28
31
|
- [Installation](#installation)
|
|
29
32
|
- [Quick start](#quick-start)
|
|
30
33
|
- [Backend: CPU, GPU, numexpr, and no-grad mode](#backend-cpu-gpu-numexpr-and-no-grad-mode)
|
|
@@ -42,12 +45,70 @@ There is no C++, no PyTorch — just NumPy and explicit, hand-derived gradients.
|
|
|
42
45
|
- [Models](#models)
|
|
43
46
|
- [Worked example: train a char-level GPT](#worked-example-train-a-char-level-gpt)
|
|
44
47
|
- [Worked example: encoder–decoder Seq2Seq](#worked-example-encoderdecoder-seq2seq)
|
|
48
|
+
- [Visualizing the computation graph](#visualizing-the-computation-graph)
|
|
45
49
|
- [Gotchas](#gotchas)
|
|
46
50
|
- [Roadmap](#roadmap)
|
|
47
51
|
- [License](#license)
|
|
48
52
|
|
|
49
53
|
---
|
|
50
54
|
|
|
55
|
+
## What's new
|
|
56
|
+
|
|
57
|
+
This version consolidates the **loss API** into three functions with a single, consistent convention — and adds **label smoothing** on the road to a full translator.
|
|
58
|
+
|
|
59
|
+
**Unified cross-entropy API (3 losses, one convention)**
|
|
60
|
+
|
|
61
|
+
Seven overlapping cross-entropy variants (`softmax_cross_entropy_old`, `softmax_cross_entropy_masked`, `sparse_softmax_cross_entropy`, `sparse_softmax_cross_entropy_legacy`, `sparse_softmax_cross_entropy_index_legacy`, `cross_entropy_loss`, `cross_entropy_loss_masked`) are replaced by three that tile the whole space with no overlap — see [Losses](#losses):
|
|
62
|
+
|
|
63
|
+
- **`Tensor.sparse_softmax_cross_entropy_index(scores, labels, mask=None, eps=0.0)`** — the workhorse: logits in, integer labels, optional padding mask, optional **label smoothing** via `eps`. No one-hot matrix is ever built.
|
|
64
|
+
- **`Tensor.softmax_cross_entropy(scores, targets, mask=None)`** — logits in, arbitrary target *distributions* (knowledge distillation, mixup, soft labels), optional mask.
|
|
65
|
+
- **`Tensor.cross_entropy_from_probs(predictions, targets, mask=None)`** — for graphs that already end in `.softmax()`; prefer the logits-based losses when you have logits.
|
|
66
|
+
|
|
67
|
+
All three now share the same rules, closing several long-standing traps:
|
|
68
|
+
|
|
69
|
+
- **One shape convention.** Every loss accepts `(N, V)` *and* `(B, T, V)` logits (auto-flattened internally); masks may be `(N,)` or `(B, T)`.
|
|
70
|
+
- **One normalization convention.** Every loss divides by the number of *real* (unmasked) tokens — no more `/B` vs `/B·T` vs `/n_real` drift between variants, which silently changed effective learning rates when switching losses.
|
|
71
|
+
- **Masks are optional everywhere**, defaulting to "all positions are real".
|
|
72
|
+
- **Exact log-probabilities.** Loss values are computed from `log-softmax` directly instead of `log(clip(p))`, so near-zero probabilities are no longer floored at `log(1e-15)`.
|
|
73
|
+
|
|
74
|
+
**Label smoothing**
|
|
75
|
+
|
|
76
|
+
`sparse_softmax_cross_entropy_index(..., eps=0.1)` trains against `(1−eps)` on the true class and `eps/(V−1)` spread uniformly over the rest — the standard regularizer for translation models — at zero extra memory: both loss and gradient are computed straight from the label indices. `eps=0.0` (default) is exact plain cross-entropy.
|
|
77
|
+
|
|
78
|
+
**Performance**
|
|
79
|
+
|
|
80
|
+
- The hot path (`eps=0`, no mask — e.g. char-level GPT training) keeps the fused, **numexpr-accelerated** in-place backward from the previous `sparse_softmax_cross_entropy`.
|
|
81
|
+
- Masked/smoothed paths fold the mask, upstream gradient, and normalization into a single per-row coefficient — one fewer `(N, V)` temporary in every backward.
|
|
82
|
+
|
|
83
|
+
**Legacy module**
|
|
84
|
+
|
|
85
|
+
The superseded losses were **moved, not deleted**: they live in `cogforge.legacy` under `TensorLegacy`, unchanged, for reference and for reproducing old experiments. New code should not import them — note that they retain their old (inconsistent) normalization conventions.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
### Previous release — autograd correctness pass
|
|
90
|
+
|
|
91
|
+
This version focused on **autograd correctness and debuggability**. All fixes below were validated with finite-difference gradient checks and end-to-end training tests.
|
|
92
|
+
|
|
93
|
+
**Gradient correctness fixes**
|
|
94
|
+
|
|
95
|
+
- **Broadcast gradients fully generalized.** `+`, `-`, and `*` previously mishandled un-broadcasting when shapes broadcast along an *interior* size-1 dimension (e.g. `(B, 1, C) + (B, T, C)` — exactly the shape of a padding mask or any `keepdims` tensor): depending on shapes this either crashed or silently accumulated wrong gradients. All elementwise ops now route through a single, verified `Tensor.unbroadcast`, matching `@`.
|
|
96
|
+
- **Indexing with repeated indices now accumulates.** `tensor[key]`'s backward used direct fancy-index assignment, which drops gradient contributions for duplicate indices due to NumPy write buffering. It now uses the backend-aware `scatter_add` (`np.add.at` on CPU, `cupyx.scatter_add` on GPU), so gathers with repeated indices — RoPE slicing, token lookups — backprop correctly.
|
|
97
|
+
- **`sparse_softmax_cross_entropy` backward rewritten.** The gradient is now written straight into `scores.grad` as `(softmax − onehot) / N` without copying or mutating the cached softmax output — faster, less memory, and verified against numerical gradients (max error ~1e-4).
|
|
98
|
+
- **`BatchNorm1D` is now safe to call multiple times before `backwards()`.** Backward state (`x_hat`, `std_inv`, the training flag) was previously stored on the module, so a second forward pass clobbered what the first pass's backward needed. Each call's backward closure now captures its own state — interleaved forwards produce bit-identical gradients to isolated ones.
|
|
99
|
+
- **`NO_GRAD` fast paths are consistent everywhere:** no op builds a graph node it's about to discard.
|
|
100
|
+
|
|
101
|
+
**Generation fixes**
|
|
102
|
+
|
|
103
|
+
- **`Seq2Seq.generate` handles per-sequence EOS.** Previously the loop only stopped if *every* row in the batch emitted `eos_id` on the *same step*, and finished rows kept sampling. Each row is now frozen to `pad_id` the moment it emits EOS, and the loop exits once all rows are done.
|
|
104
|
+
|
|
105
|
+
**New: graph visualization (`cogforge.utils`)**
|
|
106
|
+
|
|
107
|
+
- Every `Tensor` now carries a human-readable `.op` label (`"+"`, `"MATMUL"`, `"softmax"`, `"layernorm"`, …) recording the operation that produced it.
|
|
108
|
+
- `draw_graph(tensor)` renders the full computation graph via Graphviz — one record node per tensor showing its op, shape, data, and gradient. See [Visualizing the computation graph](#visualizing-the-computation-graph).
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
51
112
|
## Installation
|
|
52
113
|
|
|
53
114
|
```bash
|
|
@@ -59,15 +120,18 @@ Requires Python 3.8+ and NumPy — that's the only hard dependency. Two optional
|
|
|
59
120
|
```bash
|
|
60
121
|
pip install cupy-cuda12x # GPU backend (pick the build matching your CUDA version)
|
|
61
122
|
pip install numexpr # multi-threaded CPU element-wise ops
|
|
123
|
+
pip install graphviz # computation-graph visualization (also needs the Graphviz system binaries)
|
|
62
124
|
```
|
|
63
125
|
|
|
64
|
-
The package is organized into
|
|
126
|
+
The package is organized into four modules:
|
|
65
127
|
|
|
66
128
|
| Module | Contains |
|
|
67
129
|
| --- | --- |
|
|
68
130
|
| `cogforge.backend` | The swappable array backend: NumPy ↔ CuPy switching, numexpr flag, global no-grad flag. |
|
|
69
131
|
| `cogforge.app` | The autograd engine (`Tensor`) and every building block — layers, optimizers, losses, normalization, attention, positional encodings. |
|
|
70
132
|
| `cogforge.models` | Ready-to-use models: `GPTV1`, `GPT2`, `Seq2Seq`. |
|
|
133
|
+
| `cogforge.utils` | Computation-graph tracing and Graphviz rendering (`trace_graph`, `draw_graph`). |
|
|
134
|
+
| `cogforge.legacy` | `TensorLegacy` — the superseded cross-entropy variants, kept unchanged for reference and reproducing old experiments. Don't use in new code. |
|
|
71
135
|
|
|
72
136
|
```python
|
|
73
137
|
from cogforge.app import Tensor, Linear, Adam, MultiHeadAttention # building blocks
|
|
@@ -158,9 +222,12 @@ Tensor(array, children=(), requires_grad=True, typed="compressed")
|
|
|
158
222
|
| `children` | Parent tensors in the graph (set internally by ops; you rarely pass this). |
|
|
159
223
|
| `requires_grad` | Reserved flag (currently informational). |
|
|
160
224
|
| `typed` | `"compressed"` → `float32` (default), anything else → `float64`. |
|
|
225
|
+
| `op` | Human-readable label of the operation that produced this tensor (`"+"`, `"MATMUL"`, `"softmax"`, …). Set internally by every op; used by the graph visualizer and handy when debugging. |
|
|
161
226
|
|
|
162
227
|
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
228
|
|
|
229
|
+
> **The graph is single-use.** `backwards()` frees the graph as it goes (clearing each node's children and backward closure) to release memory eagerly. Run one forward → one backward per step, and if you want to [visualize the graph](#visualizing-the-computation-graph), draw it **before** calling `backwards()`.
|
|
230
|
+
|
|
164
231
|
---
|
|
165
232
|
|
|
166
233
|
## API reference
|
|
@@ -171,9 +238,9 @@ Gradients **accumulate** into `.grad`. Always zero them between optimization ste
|
|
|
171
238
|
|
|
172
239
|
| Operation | Notes |
|
|
173
240
|
| --- | --- |
|
|
174
|
-
| `a + b`, `a - b`, `a * b` | Elementwise, with broadcasting support. |
|
|
241
|
+
| `a + b`, `a - b`, `a * b` | Elementwise, with full broadcasting support — gradients are correctly un-broadcast for leading *and* interior size-1 dimensions. |
|
|
175
242
|
| `a @ b` | Batched matmul; gradients are correctly un-broadcast. |
|
|
176
|
-
| `a[key]` | Indexing/slicing. |
|
|
243
|
+
| `a[key]` | Indexing/slicing. Backward uses backend-aware scatter-add, so **repeated indices accumulate gradients correctly** on CPU and GPU. |
|
|
177
244
|
| `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
245
|
| `.relu()` | |
|
|
179
246
|
| `.sigmoid()` | |
|
|
@@ -191,7 +258,7 @@ Gradients **accumulate** into `.grad`. Always zero them between optimization ste
|
|
|
191
258
|
|
|
192
259
|
| Method | Notes |
|
|
193
260
|
| --- | --- |
|
|
194
|
-
| `.backwards()` | **Primary.** Iterative topological sort — safe for deep/long graphs. |
|
|
261
|
+
| `.backwards()` | **Primary.** Iterative topological sort — safe for deep/long graphs. Frees the graph as it runs (single-use; see the note under [Core concept](#core-concept-the-tensor)). |
|
|
195
262
|
| `.backwards_recursive()` | Legacy recursive version; can hit Python's recursion limit on long sequences. Prefer `.backwards()`. |
|
|
196
263
|
|
|
197
264
|
**Static helper**
|
|
@@ -202,20 +269,23 @@ Gradients **accumulate** into `.grad`. Always zero them between optimization ste
|
|
|
202
269
|
|
|
203
270
|
### Losses
|
|
204
271
|
|
|
205
|
-
All losses are **classmethods** on `Tensor` and return a scalar loss tensor you call `.backwards()` on.
|
|
272
|
+
All losses are **classmethods** on `Tensor` and return a scalar loss tensor you call `.backwards()` on. There are exactly **three**, and they share one convention:
|
|
273
|
+
|
|
274
|
+
- **Shapes:** logits/probs may be `(N, V)` or `(B, T, V)` — 3-D input is auto-flattened. Masks may be `(N,)` or `(B, T)`.
|
|
275
|
+
- **Masks:** optional everywhere (`mask=None` treats all positions as real); `1` = real token, `0` = padding. Padded positions contribute zero loss *and* exactly zero gradient.
|
|
276
|
+
- **Normalization:** always by the number of real tokens (all positions when unmasked).
|
|
277
|
+
|
|
278
|
+
Pick by what your **target** looks like and what your **input** is:
|
|
206
279
|
|
|
207
|
-
| Loss | Input
|
|
208
|
-
| --- | --- | --- |
|
|
209
|
-
| `Tensor.
|
|
210
|
-
| `Tensor.
|
|
211
|
-
| `Tensor.
|
|
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. |
|
|
280
|
+
| Loss | Input | Target | Use when |
|
|
281
|
+
| --- | --- | --- | --- |
|
|
282
|
+
| `Tensor.sparse_softmax_cross_entropy_index(scores, labels, mask=None, eps=0.0)` | **raw logits** | integer class ids | **The workhorse.** Classification, LM, seq2seq. `eps>0` enables label smoothing: `1−eps` on the true class, `eps/(V−1)` on the rest — computed straight from indices, no one-hot built. `eps=0.1` is the standard for translation. |
|
|
283
|
+
| `Tensor.softmax_cross_entropy(scores, targets, mask=None)` | **raw logits** | full distribution `(…, V)`, rows sum to 1 | Targets that aren't expressible as an index: knowledge distillation against a teacher's output, mixup, annotator vote distributions. |
|
|
284
|
+
| `Tensor.cross_entropy_from_probs(predictions, targets, mask=None)` | **probabilities** (e.g. output of `.softmax()`) | one-hot or distribution | Your graph already ends in a softmax. Prefer the logits-based losses when you have logits — their gradients are exact and bounded, this one saturates via clipping as probs → 0. |
|
|
215
285
|
|
|
216
|
-
> ℹ️ The
|
|
286
|
+
> ℹ️ The first two apply softmax internally — feed them **raw logits**. `cross_entropy_from_probs` is the opposite — it expects probabilities. Mixing these up silently trains the wrong thing.
|
|
217
287
|
>
|
|
218
|
-
> `
|
|
288
|
+
> The previous generation of losses (`softmax_cross_entropy_masked`, `sparse_softmax_cross_entropy`, `cross_entropy_loss`, `cross_entropy_loss_masked`, and the `_old`/`_legacy` variants) now lives in `cogforge.legacy` under `TensorLegacy`, unchanged. They keep their old, mutually inconsistent normalizations (`/B` vs `/B·T` vs `/n_real`) — use them only to reproduce old experiments, and don't mix them with the current losses in one training run.
|
|
219
289
|
|
|
220
290
|
---
|
|
221
291
|
|
|
@@ -231,7 +301,7 @@ Lookup table. Call with an integer index array; backward scatters gradients corr
|
|
|
231
301
|
Normalizes over the last dimension. Learnable `gamma`/`beta`, full hand-derived backward. `.parameters()` → `[gamma, beta]`.
|
|
232
302
|
|
|
233
303
|
#### `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`.
|
|
304
|
+
Normalizes over the batch (and time, for 3-D input). Tracks `running_mean`/`running_var` for inference. Toggle `.training = True/False`. Learnable `gamma`/`beta`. Each forward call captures its own backward state, so calling the layer multiple times before `backwards()` (e.g. gradient accumulation, shared modules) yields correct gradients for every call.
|
|
235
305
|
|
|
236
306
|
#### `FeedForward(dmodel, dff=None)`
|
|
237
307
|
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).
|
|
@@ -380,7 +450,7 @@ A full **encoder–decoder transformer** (the original *Attention Is All You Nee
|
|
|
380
450
|
| `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
451
|
| `model.encode(enc_idx)` | Run the encoder once; returns `(x_enc, enc_pad_mask)` for reuse across decode steps. |
|
|
382
452
|
| `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
|
|
453
|
+
| `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>`. Tracks completion **per sequence**: the moment a row emits `eos_id` it is frozen and padded with `pad_id` for the remaining steps, and decoding stops early once every row has finished. |
|
|
384
454
|
| `model.train()` / `model.infer()` | Toggle dropout across all encoder and decoder blocks. |
|
|
385
455
|
| `Seq2Seq.make_pad_mask(idx, pad_id)` | Static helper: `(B, T)` ints → `(B, 1, 1, T)` boolean mask, `True` at padding. |
|
|
386
456
|
|
|
@@ -419,7 +489,7 @@ def get_batch(bs=32):
|
|
|
419
489
|
for step in range(2000):
|
|
420
490
|
x, y = get_batch()
|
|
421
491
|
logits = model(x) # (B, T, vocab)
|
|
422
|
-
loss = Tensor.
|
|
492
|
+
loss = Tensor.sparse_softmax_cross_entropy_index(logits, y)
|
|
423
493
|
|
|
424
494
|
opt.zero_grad()
|
|
425
495
|
loss.backwards()
|
|
@@ -472,25 +542,56 @@ for step in range(num_steps):
|
|
|
472
542
|
dec_in, labels = tgt[:, :-1], tgt[:, 1:]
|
|
473
543
|
logits = model(src, dec_in) # (B, T_dec, vocab)
|
|
474
544
|
|
|
475
|
-
mask = (labels != PAD).astype(np.float32)
|
|
476
|
-
loss = Tensor.sparse_softmax_cross_entropy_index(logits, labels,
|
|
545
|
+
mask = (labels != PAD).astype(np.float32) # (B, T_dec): 1 = real, 0 = pad
|
|
546
|
+
loss = Tensor.sparse_softmax_cross_entropy_index(logits, labels,
|
|
547
|
+
mask=mask, eps=0.1) # label smoothing
|
|
477
548
|
|
|
478
549
|
opt.zero_grad()
|
|
479
550
|
loss.backwards()
|
|
480
551
|
opt.clip_grads(1.0)
|
|
481
552
|
opt.step()
|
|
482
553
|
|
|
483
|
-
# inference: encode once, decode token by token, stop on EOS
|
|
554
|
+
# inference: encode once, decode token by token, stop on EOS (per sequence)
|
|
484
555
|
out = model.generate(src, sos_id=SOS, eos_id=EOS, max_new=32,
|
|
485
556
|
temperature=1.0, top_k=5)
|
|
486
557
|
```
|
|
487
558
|
|
|
488
559
|
---
|
|
489
560
|
|
|
561
|
+
## Visualizing the computation graph
|
|
562
|
+
|
|
563
|
+
`cogforge.utils` renders the autograd graph with Graphviz — every node shows the op that produced it, its shape, its data, and its gradient. It's the fastest way to *see* what your model is doing and to debug shape or gradient-flow issues.
|
|
564
|
+
|
|
565
|
+
```python
|
|
566
|
+
import numpy as np
|
|
567
|
+
from cogforge.app import Tensor, Linear
|
|
568
|
+
from cogforge.utils import draw_graph
|
|
569
|
+
|
|
570
|
+
x = Tensor(np.random.randn(2, 4))
|
|
571
|
+
lin = Linear(4, 3)
|
|
572
|
+
out = lin(x).relu().softmax()
|
|
573
|
+
|
|
574
|
+
dot = draw_graph(out, rankdir='LR') # draw BEFORE backwards() — the graph is freed by it
|
|
575
|
+
dot.render('graph', view=True) # writes graph.svg and opens it
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
| Function | Purpose |
|
|
579
|
+
| --- | --- |
|
|
580
|
+
| `trace_graph(root_tensor)` | Walks the graph from a root tensor; returns `(nodes, edges)`. |
|
|
581
|
+
| `draw_graph(root_tensor, max_char=50, format='svg', rankdir='LR')` | Builds a Graphviz `Digraph`. `max_char` truncates long data/grad strings; `format` is any Graphviz output format (`svg`, `png`, `pdf`); `rankdir='TB'` for top-to-bottom layout. |
|
|
582
|
+
|
|
583
|
+
Nodes produced by a named op render white with the op label; raw leaf tensors (parameters, inputs) render light blue. Requires the `graphviz` Python package **and** the Graphviz system binaries (`apt install graphviz` / `brew install graphviz`).
|
|
584
|
+
|
|
585
|
+
Two practical notes: call `draw_graph` **before** `backwards()`, since the backward pass frees the graph as it runs (afterwards you'll see a single orphan node); and keep the visualized graph small — a full transformer forward produces hundreds of nodes, so draw a single block or a toy input rather than a whole training step.
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
490
589
|
## Gotchas
|
|
491
590
|
|
|
492
591
|
- **It's `backwards()`, not `backward()`.** The backward pass method has a trailing `s`.
|
|
493
|
-
- **Logits vs. probabilities.** `softmax_cross_entropy`
|
|
592
|
+
- **Logits vs. probabilities.** `softmax_cross_entropy` and `sparse_softmax_cross_entropy_index` fuse the softmax internally — feed them **raw logits**. `cross_entropy_from_probs` expects **probabilities**. Mixing these up silently trains the wrong thing.
|
|
593
|
+
- **Label smoothing raises the loss floor.** With `eps > 0` the minimum achievable loss is no longer 0 (a perfect model still pays the smoothing term), so judge training by token accuracy or validation metrics, not by how close the raw loss gets to zero — and don't compare loss values across different `eps` settings.
|
|
594
|
+
- **Legacy losses normalize differently.** `cogforge.legacy.TensorLegacy` keeps the old `/B` / `/B·T` conventions; the current losses always divide by real-token count. Swapping one for the other rescales gradients — retune the learning rate if you migrate an old script.
|
|
494
595
|
- **Gradients accumulate.** Call `optimizer.zero_grad()` every step (or `p.grad[...] = 0`), or gradients pile up across iterations.
|
|
495
596
|
- **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
597
|
- **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.
|
|
@@ -498,13 +599,26 @@ out = model.generate(src, sos_id=SOS, eos_id=EOS, max_new=32,
|
|
|
498
599
|
- **`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
600
|
- **RNNs operate on lists**, not a single `(B, T, C)` tensor — pass a list of per-timestep tensors.
|
|
500
601
|
- **Don't dedupe tied parameters yourself.** `Seq2Seq.parameters()` already deduplicates shared tensors by identity, so the tied embedding is only updated once per step.
|
|
602
|
+
- **The graph is single-use.** `backwards()` frees children and backward closures as it runs. One forward → one backward. Calling `backwards()` twice on the same graph is a silent no-op the second time, and `draw_graph` must be called before, not after.
|
|
603
|
+
- **Visualize small graphs.** `draw_graph` on a full model forward will produce an unreadable diagram with hundreds of nodes; visualize a single layer or block instead.
|
|
501
604
|
|
|
502
605
|
---
|
|
503
606
|
|
|
504
607
|
## Roadmap
|
|
505
608
|
|
|
506
|
-
Shipped
|
|
609
|
+
Shipped in this release:
|
|
610
|
+
|
|
611
|
+
- ✅ **Unified loss API** — three cross-entropy losses covering index targets, distribution targets, and probability inputs, with one shape and normalization convention (see [What's new](#whats-new))
|
|
612
|
+
- ✅ **Label smoothing** (`eps`) in `sparse_softmax_cross_entropy_index`, computed from indices with zero extra memory
|
|
613
|
+
- ✅ Optional padding masks and `(B, T, V)` auto-flattening in every loss
|
|
614
|
+
- ✅ `cogforge.legacy` module (`TensorLegacy`) preserving the superseded losses for reproducibility
|
|
615
|
+
|
|
616
|
+
Shipped previously:
|
|
507
617
|
|
|
618
|
+
- ✅ **Autograd correctness pass** — all gradients verified against finite-difference checks; broadcast, indexing, sparse cross-entropy, and BatchNorm backward bugs fixed
|
|
619
|
+
- ✅ Computation-graph visualization via Graphviz (`cogforge.utils`), with per-tensor `op` labels
|
|
620
|
+
- ✅ Per-sequence EOS handling in `Seq2Seq.generate` (finished rows freeze to `pad_id`)
|
|
621
|
+
- ✅ Eager graph freeing in `backwards()` for lower peak memory
|
|
508
622
|
- ✅ RoPE (rotary position embeddings), usable in GPT and at every attention site of the Seq2Seq model
|
|
509
623
|
- ✅ Full encoder–decoder transformer (`Seq2Seq`) with cross-attention and padding masks
|
|
510
624
|
- ✅ Weight tying between embeddings and the output head
|
|
@@ -514,14 +628,19 @@ Shipped since the last release:
|
|
|
514
628
|
|
|
515
629
|
Planned / under consideration:
|
|
516
630
|
|
|
631
|
+
- Beam search decoding for `Seq2Seq` (length-normalized, per-sequence finished pool)
|
|
632
|
+
- Lazy Gradient Allocation (LGA) for lower peak memory on long sequences
|
|
633
|
+
- Cosine LR function as part of UTILS module
|
|
634
|
+
- Weight decay / AdamW optimizer
|
|
635
|
+
- Checkpoint save/load (`state_dict` / `load_state_dict`) for the transformer models, including optimizer state
|
|
636
|
+
- `no_grad()` context manager and a shared `Module` base class
|
|
517
637
|
- KV cache for faster generation
|
|
518
638
|
- SwiGLU feed-forward and RMSNorm
|
|
519
639
|
- RoPE length interpolation
|
|
520
640
|
- Linear-attention block (as a study in the recall-vs-cost tradeoff)
|
|
521
|
-
- Checkpoint save/load for the transformer models
|
|
522
641
|
|
|
523
642
|
---
|
|
524
643
|
|
|
525
644
|
## License
|
|
526
645
|
|
|
527
|
-
|
|
646
|
+
APACHE License. See [LICENSE](LICENSE) for details.
|