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.
@@ -1,6 +1,7 @@
1
1
  # Python caches
2
2
  __pycache__/
3
3
  *.py[cod]
4
+ .gitignore
4
5
 
5
6
  # Build folders (created by PyPI later)
6
7
  dist/
@@ -9,4 +10,7 @@ build/
9
10
 
10
11
  # Saved Model Weights
11
12
  *.npz
12
- *.pt
13
+ *.pt
14
+
15
+ project_review_report.md
16
+ ROADMAP.md
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cogforge-engine
3
- Version: 2.1.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 three modules:
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. Mind the distinction between losses that take **probabilities** and losses that take **raw logits** — this is the most common mistake.
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 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. |
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 `softmax_*` and `sparse_softmax_*` variants apply softmax internally — feed them **raw logits**. `cross_entropy_loss*` is the opposite — it expects probabilities.
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
- > `sparse_softmax_cross_entropy_legacy` and `softmax_cross_entropy_old` are kept for reference; prefer the current versions.
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>`; stops early when every sequence in the batch emits `eos_id`. |
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.sparse_softmax_cross_entropy(logits, y)
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, mask)
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` / `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.
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 since the last release:
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
- MIT License. See [LICENSE](LICENSE) for details.
646
+ APACHE License. See [LICENSE](LICENSE) for details.