cogforge-engine 2.1.0__tar.gz → 2.1.2__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 → cogforge_engine-2.1.2}/PKG-INFO +85 -12
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/README.md +84 -11
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/cogforge/app.py +18 -21
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/cogforge/models.py +10 -3
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/cogforge/utils.py +23 -4
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/pyproject.toml +1 -1
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/.github/workflows/release.yml +0 -0
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/.gitignore +0 -0
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/LICENSE +0 -0
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/TODO +0 -0
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/cogforge/__init__.py +0 -0
- {cogforge_engine-2.1.0 → cogforge_engine-2.1.2}/cogforge/backend.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.2
|
|
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 is a correctness pass.** Every gradient in the engine has now been verified against finite-difference numerical checks, and several subtle autograd bugs were found and fixed — 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,36 @@ 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 focuses on **autograd correctness and debuggability**. All fixes below were validated with finite-difference gradient checks and end-to-end training tests.
|
|
58
|
+
|
|
59
|
+
**Gradient correctness fixes**
|
|
60
|
+
|
|
61
|
+
- **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 `@`.
|
|
62
|
+
- **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.
|
|
63
|
+
- **`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).
|
|
64
|
+
- **`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.
|
|
65
|
+
- **`NO_GRAD` fast paths are consistent everywhere:** no op builds a graph node it's about to discard.
|
|
66
|
+
|
|
67
|
+
**Generation fixes**
|
|
68
|
+
|
|
69
|
+
- **`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.
|
|
70
|
+
|
|
71
|
+
**New: graph visualization (`cogforge.utils`)**
|
|
72
|
+
|
|
73
|
+
- Every `Tensor` now carries a human-readable `.op` label (`"+"`, `"MATMUL"`, `"softmax"`, `"layernorm"`, …) recording the operation that produced it.
|
|
74
|
+
- `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).
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
51
78
|
## Installation
|
|
52
79
|
|
|
53
80
|
```bash
|
|
@@ -59,15 +86,17 @@ Requires Python 3.8+ and NumPy — that's the only hard dependency. Two optional
|
|
|
59
86
|
```bash
|
|
60
87
|
pip install cupy-cuda12x # GPU backend (pick the build matching your CUDA version)
|
|
61
88
|
pip install numexpr # multi-threaded CPU element-wise ops
|
|
89
|
+
pip install graphviz # computation-graph visualization (also needs the Graphviz system binaries)
|
|
62
90
|
```
|
|
63
91
|
|
|
64
|
-
The package is organized into
|
|
92
|
+
The package is organized into four modules:
|
|
65
93
|
|
|
66
94
|
| Module | Contains |
|
|
67
95
|
| --- | --- |
|
|
68
96
|
| `cogforge.backend` | The swappable array backend: NumPy ↔ CuPy switching, numexpr flag, global no-grad flag. |
|
|
69
97
|
| `cogforge.app` | The autograd engine (`Tensor`) and every building block — layers, optimizers, losses, normalization, attention, positional encodings. |
|
|
70
98
|
| `cogforge.models` | Ready-to-use models: `GPTV1`, `GPT2`, `Seq2Seq`. |
|
|
99
|
+
| `cogforge.utils` | Computation-graph tracing and Graphviz rendering (`trace_graph`, `draw_graph`). |
|
|
71
100
|
|
|
72
101
|
```python
|
|
73
102
|
from cogforge.app import Tensor, Linear, Adam, MultiHeadAttention # building blocks
|
|
@@ -158,9 +187,12 @@ Tensor(array, children=(), requires_grad=True, typed="compressed")
|
|
|
158
187
|
| `children` | Parent tensors in the graph (set internally by ops; you rarely pass this). |
|
|
159
188
|
| `requires_grad` | Reserved flag (currently informational). |
|
|
160
189
|
| `typed` | `"compressed"` → `float32` (default), anything else → `float64`. |
|
|
190
|
+
| `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
191
|
|
|
162
192
|
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
193
|
|
|
194
|
+
> **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()`.
|
|
195
|
+
|
|
164
196
|
---
|
|
165
197
|
|
|
166
198
|
## API reference
|
|
@@ -171,9 +203,9 @@ Gradients **accumulate** into `.grad`. Always zero them between optimization ste
|
|
|
171
203
|
|
|
172
204
|
| Operation | Notes |
|
|
173
205
|
| --- | --- |
|
|
174
|
-
| `a + b`, `a - b`, `a * b` | Elementwise, with broadcasting support. |
|
|
206
|
+
| `a + b`, `a - b`, `a * b` | Elementwise, with full broadcasting support — gradients are correctly un-broadcast for leading *and* interior size-1 dimensions. |
|
|
175
207
|
| `a @ b` | Batched matmul; gradients are correctly un-broadcast. |
|
|
176
|
-
| `a[key]` | Indexing/slicing. |
|
|
208
|
+
| `a[key]` | Indexing/slicing. Backward uses backend-aware scatter-add, so **repeated indices accumulate gradients correctly** on CPU and GPU. |
|
|
177
209
|
| `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
210
|
| `.relu()` | |
|
|
179
211
|
| `.sigmoid()` | |
|
|
@@ -191,7 +223,7 @@ Gradients **accumulate** into `.grad`. Always zero them between optimization ste
|
|
|
191
223
|
|
|
192
224
|
| Method | Notes |
|
|
193
225
|
| --- | --- |
|
|
194
|
-
| `.backwards()` | **Primary.** Iterative topological sort — safe for deep/long graphs. |
|
|
226
|
+
| `.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
227
|
| `.backwards_recursive()` | Legacy recursive version; can hit Python's recursion limit on long sequences. Prefer `.backwards()`. |
|
|
196
228
|
|
|
197
229
|
**Static helper**
|
|
@@ -231,7 +263,7 @@ Lookup table. Call with an integer index array; backward scatters gradients corr
|
|
|
231
263
|
Normalizes over the last dimension. Learnable `gamma`/`beta`, full hand-derived backward. `.parameters()` → `[gamma, beta]`.
|
|
232
264
|
|
|
233
265
|
#### `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`.
|
|
266
|
+
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
267
|
|
|
236
268
|
#### `FeedForward(dmodel, dff=None)`
|
|
237
269
|
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 +412,7 @@ A full **encoder–decoder transformer** (the original *Attention Is All You Nee
|
|
|
380
412
|
| `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
413
|
| `model.encode(enc_idx)` | Run the encoder once; returns `(x_enc, enc_pad_mask)` for reuse across decode steps. |
|
|
382
414
|
| `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
|
|
415
|
+
| `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
416
|
| `model.train()` / `model.infer()` | Toggle dropout across all encoder and decoder blocks. |
|
|
385
417
|
| `Seq2Seq.make_pad_mask(idx, pad_id)` | Static helper: `(B, T)` ints → `(B, 1, 1, T)` boolean mask, `True` at padding. |
|
|
386
418
|
|
|
@@ -480,13 +512,41 @@ for step in range(num_steps):
|
|
|
480
512
|
opt.clip_grads(1.0)
|
|
481
513
|
opt.step()
|
|
482
514
|
|
|
483
|
-
# inference: encode once, decode token by token, stop on EOS
|
|
515
|
+
# inference: encode once, decode token by token, stop on EOS (per sequence)
|
|
484
516
|
out = model.generate(src, sos_id=SOS, eos_id=EOS, max_new=32,
|
|
485
517
|
temperature=1.0, top_k=5)
|
|
486
518
|
```
|
|
487
519
|
|
|
488
520
|
---
|
|
489
521
|
|
|
522
|
+
## Visualizing the computation graph
|
|
523
|
+
|
|
524
|
+
`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.
|
|
525
|
+
|
|
526
|
+
```python
|
|
527
|
+
import numpy as np
|
|
528
|
+
from cogforge.app import Tensor, Linear
|
|
529
|
+
from cogforge.utils import draw_graph
|
|
530
|
+
|
|
531
|
+
x = Tensor(np.random.randn(2, 4))
|
|
532
|
+
lin = Linear(4, 3)
|
|
533
|
+
out = lin(x).relu().softmax()
|
|
534
|
+
|
|
535
|
+
dot = draw_graph(out, rankdir='LR') # draw BEFORE backwards() — the graph is freed by it
|
|
536
|
+
dot.render('graph', view=True) # writes graph.svg and opens it
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
| Function | Purpose |
|
|
540
|
+
| --- | --- |
|
|
541
|
+
| `trace_graph(root_tensor)` | Walks the graph from a root tensor; returns `(nodes, edges)`. |
|
|
542
|
+
| `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. |
|
|
543
|
+
|
|
544
|
+
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`).
|
|
545
|
+
|
|
546
|
+
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.
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
490
550
|
## Gotchas
|
|
491
551
|
|
|
492
552
|
- **It's `backwards()`, not `backward()`.** The backward pass method has a trailing `s`.
|
|
@@ -498,12 +558,21 @@ out = model.generate(src, sos_id=SOS, eos_id=EOS, max_new=32,
|
|
|
498
558
|
- **`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
559
|
- **RNNs operate on lists**, not a single `(B, T, C)` tensor — pass a list of per-timestep tensors.
|
|
500
560
|
- **Don't dedupe tied parameters yourself.** `Seq2Seq.parameters()` already deduplicates shared tensors by identity, so the tied embedding is only updated once per step.
|
|
561
|
+
- **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.
|
|
562
|
+
- **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
563
|
|
|
502
564
|
---
|
|
503
565
|
|
|
504
566
|
## Roadmap
|
|
505
567
|
|
|
506
|
-
Shipped
|
|
568
|
+
Shipped in this release:
|
|
569
|
+
|
|
570
|
+
- ✅ **Autograd correctness pass** — all gradients verified against finite-difference checks; broadcast, indexing, sparse cross-entropy, and BatchNorm backward bugs fixed (see [What's new](#whats-new))
|
|
571
|
+
- ✅ Computation-graph visualization via Graphviz (`cogforge.utils`), with per-tensor `op` labels
|
|
572
|
+
- ✅ Per-sequence EOS handling in `Seq2Seq.generate` (finished rows freeze to `pad_id`)
|
|
573
|
+
- ✅ Eager graph freeing in `backwards()` for lower peak memory
|
|
574
|
+
|
|
575
|
+
Shipped previously:
|
|
507
576
|
|
|
508
577
|
- ✅ RoPE (rotary position embeddings), usable in GPT and at every attention site of the Seq2Seq model
|
|
509
578
|
- ✅ Full encoder–decoder transformer (`Seq2Seq`) with cross-attention and padding masks
|
|
@@ -514,14 +583,18 @@ Shipped since the last release:
|
|
|
514
583
|
|
|
515
584
|
Planned / under consideration:
|
|
516
585
|
|
|
586
|
+
- Lazy Gradient Allocation (LGA) for lower peak memory on long sequences
|
|
587
|
+
- Cosine LR function as part of UTILS module
|
|
588
|
+
- Weight decay / AdamW optimizer
|
|
589
|
+
- Checkpoint save/load (`state_dict` / `load_state_dict`) for the transformer models, including optimizer state
|
|
590
|
+
- `no_grad()` context manager and a shared `Module` base class
|
|
517
591
|
- KV cache for faster generation
|
|
518
592
|
- SwiGLU feed-forward and RMSNorm
|
|
519
593
|
- RoPE length interpolation
|
|
520
594
|
- Linear-attention block (as a study in the recall-vs-cost tradeoff)
|
|
521
|
-
- Checkpoint save/load for the transformer models
|
|
522
595
|
|
|
523
596
|
---
|
|
524
597
|
|
|
525
598
|
## License
|
|
526
599
|
|
|
527
|
-
|
|
600
|
+
APACHE License. See [LICENSE](LICENSE) for details.
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
`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.
|
|
6
6
|
|
|
7
|
-
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.
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
> **This release is a correctness pass.** Every gradient in the engine has now been verified against finite-difference numerical checks, and several subtle autograd bugs were found and fixed — see [What's new](#whats-new).
|
|
8
10
|
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
## Table of contents
|
|
12
14
|
|
|
15
|
+
- [What's new](#whats-new)
|
|
13
16
|
- [Installation](#installation)
|
|
14
17
|
- [Quick start](#quick-start)
|
|
15
18
|
- [Backend: CPU, GPU, numexpr, and no-grad mode](#backend-cpu-gpu-numexpr-and-no-grad-mode)
|
|
@@ -27,12 +30,36 @@ There is no C++, no PyTorch — just NumPy and explicit, hand-derived gradients.
|
|
|
27
30
|
- [Models](#models)
|
|
28
31
|
- [Worked example: train a char-level GPT](#worked-example-train-a-char-level-gpt)
|
|
29
32
|
- [Worked example: encoder–decoder Seq2Seq](#worked-example-encoderdecoder-seq2seq)
|
|
33
|
+
- [Visualizing the computation graph](#visualizing-the-computation-graph)
|
|
30
34
|
- [Gotchas](#gotchas)
|
|
31
35
|
- [Roadmap](#roadmap)
|
|
32
36
|
- [License](#license)
|
|
33
37
|
|
|
34
38
|
---
|
|
35
39
|
|
|
40
|
+
## What's new
|
|
41
|
+
|
|
42
|
+
This version focuses on **autograd correctness and debuggability**. All fixes below were validated with finite-difference gradient checks and end-to-end training tests.
|
|
43
|
+
|
|
44
|
+
**Gradient correctness fixes**
|
|
45
|
+
|
|
46
|
+
- **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 `@`.
|
|
47
|
+
- **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.
|
|
48
|
+
- **`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).
|
|
49
|
+
- **`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.
|
|
50
|
+
- **`NO_GRAD` fast paths are consistent everywhere:** no op builds a graph node it's about to discard.
|
|
51
|
+
|
|
52
|
+
**Generation fixes**
|
|
53
|
+
|
|
54
|
+
- **`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.
|
|
55
|
+
|
|
56
|
+
**New: graph visualization (`cogforge.utils`)**
|
|
57
|
+
|
|
58
|
+
- Every `Tensor` now carries a human-readable `.op` label (`"+"`, `"MATMUL"`, `"softmax"`, `"layernorm"`, …) recording the operation that produced it.
|
|
59
|
+
- `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).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
36
63
|
## Installation
|
|
37
64
|
|
|
38
65
|
```bash
|
|
@@ -44,15 +71,17 @@ Requires Python 3.8+ and NumPy — that's the only hard dependency. Two optional
|
|
|
44
71
|
```bash
|
|
45
72
|
pip install cupy-cuda12x # GPU backend (pick the build matching your CUDA version)
|
|
46
73
|
pip install numexpr # multi-threaded CPU element-wise ops
|
|
74
|
+
pip install graphviz # computation-graph visualization (also needs the Graphviz system binaries)
|
|
47
75
|
```
|
|
48
76
|
|
|
49
|
-
The package is organized into
|
|
77
|
+
The package is organized into four modules:
|
|
50
78
|
|
|
51
79
|
| Module | Contains |
|
|
52
80
|
| --- | --- |
|
|
53
81
|
| `cogforge.backend` | The swappable array backend: NumPy ↔ CuPy switching, numexpr flag, global no-grad flag. |
|
|
54
82
|
| `cogforge.app` | The autograd engine (`Tensor`) and every building block — layers, optimizers, losses, normalization, attention, positional encodings. |
|
|
55
83
|
| `cogforge.models` | Ready-to-use models: `GPTV1`, `GPT2`, `Seq2Seq`. |
|
|
84
|
+
| `cogforge.utils` | Computation-graph tracing and Graphviz rendering (`trace_graph`, `draw_graph`). |
|
|
56
85
|
|
|
57
86
|
```python
|
|
58
87
|
from cogforge.app import Tensor, Linear, Adam, MultiHeadAttention # building blocks
|
|
@@ -143,9 +172,12 @@ Tensor(array, children=(), requires_grad=True, typed="compressed")
|
|
|
143
172
|
| `children` | Parent tensors in the graph (set internally by ops; you rarely pass this). |
|
|
144
173
|
| `requires_grad` | Reserved flag (currently informational). |
|
|
145
174
|
| `typed` | `"compressed"` → `float32` (default), anything else → `float64`. |
|
|
175
|
+
| `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. |
|
|
146
176
|
|
|
147
177
|
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.
|
|
148
178
|
|
|
179
|
+
> **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()`.
|
|
180
|
+
|
|
149
181
|
---
|
|
150
182
|
|
|
151
183
|
## API reference
|
|
@@ -156,9 +188,9 @@ Gradients **accumulate** into `.grad`. Always zero them between optimization ste
|
|
|
156
188
|
|
|
157
189
|
| Operation | Notes |
|
|
158
190
|
| --- | --- |
|
|
159
|
-
| `a + b`, `a - b`, `a * b` | Elementwise, with broadcasting support. |
|
|
191
|
+
| `a + b`, `a - b`, `a * b` | Elementwise, with full broadcasting support — gradients are correctly un-broadcast for leading *and* interior size-1 dimensions. |
|
|
160
192
|
| `a @ b` | Batched matmul; gradients are correctly un-broadcast. |
|
|
161
|
-
| `a[key]` | Indexing/slicing. |
|
|
193
|
+
| `a[key]` | Indexing/slicing. Backward uses backend-aware scatter-add, so **repeated indices accumulate gradients correctly** on CPU and GPU. |
|
|
162
194
|
| `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.) |
|
|
163
195
|
| `.relu()` | |
|
|
164
196
|
| `.sigmoid()` | |
|
|
@@ -176,7 +208,7 @@ Gradients **accumulate** into `.grad`. Always zero them between optimization ste
|
|
|
176
208
|
|
|
177
209
|
| Method | Notes |
|
|
178
210
|
| --- | --- |
|
|
179
|
-
| `.backwards()` | **Primary.** Iterative topological sort — safe for deep/long graphs. |
|
|
211
|
+
| `.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)). |
|
|
180
212
|
| `.backwards_recursive()` | Legacy recursive version; can hit Python's recursion limit on long sequences. Prefer `.backwards()`. |
|
|
181
213
|
|
|
182
214
|
**Static helper**
|
|
@@ -216,7 +248,7 @@ Lookup table. Call with an integer index array; backward scatters gradients corr
|
|
|
216
248
|
Normalizes over the last dimension. Learnable `gamma`/`beta`, full hand-derived backward. `.parameters()` → `[gamma, beta]`.
|
|
217
249
|
|
|
218
250
|
#### `BatchNorm1D(dim, eps=1e-5, momentum=0.1)`
|
|
219
|
-
Normalizes over the batch (and time, for 3-D input). Tracks `running_mean`/`running_var` for inference. Toggle `.training = True/False`. Learnable `gamma`/`beta`.
|
|
251
|
+
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.
|
|
220
252
|
|
|
221
253
|
#### `FeedForward(dmodel, dff=None)`
|
|
222
254
|
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).
|
|
@@ -365,7 +397,7 @@ A full **encoder–decoder transformer** (the original *Attention Is All You Nee
|
|
|
365
397
|
| `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)`. |
|
|
366
398
|
| `model.encode(enc_idx)` | Run the encoder once; returns `(x_enc, enc_pad_mask)` for reuse across decode steps. |
|
|
367
399
|
| `model.decode_step(dec_idx, x_enc, enc_pad)` | Decoder forward against a fixed encoder output. |
|
|
368
|
-
| `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
|
|
400
|
+
| `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. |
|
|
369
401
|
| `model.train()` / `model.infer()` | Toggle dropout across all encoder and decoder blocks. |
|
|
370
402
|
| `Seq2Seq.make_pad_mask(idx, pad_id)` | Static helper: `(B, T)` ints → `(B, 1, 1, T)` boolean mask, `True` at padding. |
|
|
371
403
|
|
|
@@ -465,13 +497,41 @@ for step in range(num_steps):
|
|
|
465
497
|
opt.clip_grads(1.0)
|
|
466
498
|
opt.step()
|
|
467
499
|
|
|
468
|
-
# inference: encode once, decode token by token, stop on EOS
|
|
500
|
+
# inference: encode once, decode token by token, stop on EOS (per sequence)
|
|
469
501
|
out = model.generate(src, sos_id=SOS, eos_id=EOS, max_new=32,
|
|
470
502
|
temperature=1.0, top_k=5)
|
|
471
503
|
```
|
|
472
504
|
|
|
473
505
|
---
|
|
474
506
|
|
|
507
|
+
## Visualizing the computation graph
|
|
508
|
+
|
|
509
|
+
`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.
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
import numpy as np
|
|
513
|
+
from cogforge.app import Tensor, Linear
|
|
514
|
+
from cogforge.utils import draw_graph
|
|
515
|
+
|
|
516
|
+
x = Tensor(np.random.randn(2, 4))
|
|
517
|
+
lin = Linear(4, 3)
|
|
518
|
+
out = lin(x).relu().softmax()
|
|
519
|
+
|
|
520
|
+
dot = draw_graph(out, rankdir='LR') # draw BEFORE backwards() — the graph is freed by it
|
|
521
|
+
dot.render('graph', view=True) # writes graph.svg and opens it
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
| Function | Purpose |
|
|
525
|
+
| --- | --- |
|
|
526
|
+
| `trace_graph(root_tensor)` | Walks the graph from a root tensor; returns `(nodes, edges)`. |
|
|
527
|
+
| `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. |
|
|
528
|
+
|
|
529
|
+
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`).
|
|
530
|
+
|
|
531
|
+
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.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
475
535
|
## Gotchas
|
|
476
536
|
|
|
477
537
|
- **It's `backwards()`, not `backward()`.** The backward pass method has a trailing `s`.
|
|
@@ -483,12 +543,21 @@ out = model.generate(src, sos_id=SOS, eos_id=EOS, max_new=32,
|
|
|
483
543
|
- **`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).
|
|
484
544
|
- **RNNs operate on lists**, not a single `(B, T, C)` tensor — pass a list of per-timestep tensors.
|
|
485
545
|
- **Don't dedupe tied parameters yourself.** `Seq2Seq.parameters()` already deduplicates shared tensors by identity, so the tied embedding is only updated once per step.
|
|
546
|
+
- **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.
|
|
547
|
+
- **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.
|
|
486
548
|
|
|
487
549
|
---
|
|
488
550
|
|
|
489
551
|
## Roadmap
|
|
490
552
|
|
|
491
|
-
Shipped
|
|
553
|
+
Shipped in this release:
|
|
554
|
+
|
|
555
|
+
- ✅ **Autograd correctness pass** — all gradients verified against finite-difference checks; broadcast, indexing, sparse cross-entropy, and BatchNorm backward bugs fixed (see [What's new](#whats-new))
|
|
556
|
+
- ✅ Computation-graph visualization via Graphviz (`cogforge.utils`), with per-tensor `op` labels
|
|
557
|
+
- ✅ Per-sequence EOS handling in `Seq2Seq.generate` (finished rows freeze to `pad_id`)
|
|
558
|
+
- ✅ Eager graph freeing in `backwards()` for lower peak memory
|
|
559
|
+
|
|
560
|
+
Shipped previously:
|
|
492
561
|
|
|
493
562
|
- ✅ RoPE (rotary position embeddings), usable in GPT and at every attention site of the Seq2Seq model
|
|
494
563
|
- ✅ Full encoder–decoder transformer (`Seq2Seq`) with cross-attention and padding masks
|
|
@@ -499,14 +568,18 @@ Shipped since the last release:
|
|
|
499
568
|
|
|
500
569
|
Planned / under consideration:
|
|
501
570
|
|
|
571
|
+
- Lazy Gradient Allocation (LGA) for lower peak memory on long sequences
|
|
572
|
+
- Cosine LR function as part of UTILS module
|
|
573
|
+
- Weight decay / AdamW optimizer
|
|
574
|
+
- Checkpoint save/load (`state_dict` / `load_state_dict`) for the transformer models, including optimizer state
|
|
575
|
+
- `no_grad()` context manager and a shared `Module` base class
|
|
502
576
|
- KV cache for faster generation
|
|
503
577
|
- SwiGLU feed-forward and RMSNorm
|
|
504
578
|
- RoPE length interpolation
|
|
505
579
|
- Linear-attention block (as a study in the recall-vs-cost tradeoff)
|
|
506
|
-
- Checkpoint save/load for the transformer models
|
|
507
580
|
|
|
508
581
|
---
|
|
509
582
|
|
|
510
583
|
## License
|
|
511
584
|
|
|
512
|
-
|
|
585
|
+
APACHE License. See [LICENSE](LICENSE) for details.
|
|
@@ -857,49 +857,46 @@ class BatchNorm1D:
|
|
|
857
857
|
|
|
858
858
|
def __call__(self, x: Tensor):
|
|
859
859
|
reduce_dims = (0,) if x.data.ndim == 2 else (0, 1)
|
|
860
|
-
|
|
860
|
+
|
|
861
861
|
if self.training:
|
|
862
862
|
mean = x.data.mean(axis=reduce_dims, keepdims=True)
|
|
863
|
-
var
|
|
864
|
-
|
|
863
|
+
var = x.data.var(axis=reduce_dims, keepdims=True)
|
|
864
|
+
|
|
865
|
+
x_centered = x.data - mean # local, not self.
|
|
865
866
|
std_inv = 1.0 / backend.np.sqrt(var + self.eps)
|
|
866
867
|
x_hat = x_centered * std_inv
|
|
867
|
-
|
|
868
|
+
|
|
868
869
|
N = numpy.prod([x.data.shape[d] for d in reduce_dims])
|
|
869
870
|
unbiased_var = var.squeeze() * (N / (N - 1)) if N > 1 else var.squeeze()
|
|
870
|
-
|
|
871
871
|
self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * mean.squeeze()
|
|
872
|
-
self.running_var
|
|
873
|
-
|
|
872
|
+
self.running_var = (1 - self.momentum) * self.running_var + self.momentum * unbiased_var
|
|
874
873
|
else:
|
|
875
|
-
std_inv =
|
|
876
|
-
x_hat = (x.data - self.running_mean)
|
|
877
|
-
|
|
874
|
+
std_inv = None
|
|
875
|
+
x_hat = (x.data - self.running_mean) / backend.np.sqrt(self.running_var + self.eps)
|
|
876
|
+
|
|
878
877
|
out_data = self.gamma.data * x_hat + self.beta.data
|
|
879
878
|
if backend.NO_GRAD: return Tensor(out_data, op="BatchNorm")
|
|
880
879
|
out = Tensor(out_data, children=(x, self.gamma, self.beta), op="BatchNorm")
|
|
881
|
-
|
|
880
|
+
|
|
881
|
+
was_training = self.training
|
|
882
|
+
|
|
882
883
|
def _backward():
|
|
883
|
-
if not
|
|
884
|
+
if not was_training:
|
|
884
885
|
return
|
|
885
|
-
|
|
886
886
|
dout = out.grad
|
|
887
887
|
self.gamma.grad += backend.np.sum(dout * x_hat, axis=reduce_dims)
|
|
888
|
-
self.beta.grad
|
|
889
|
-
|
|
888
|
+
self.beta.grad += backend.np.sum(dout, axis=reduce_dims)
|
|
890
889
|
N = numpy.prod([x.data.shape[d] for d in reduce_dims])
|
|
891
|
-
|
|
892
890
|
dx_hat = dout * self.gamma.data
|
|
893
891
|
dx = (1.0 / N) * std_inv * (
|
|
894
|
-
N * dx_hat
|
|
895
|
-
- backend.np.sum(dx_hat, axis=reduce_dims, keepdims=True)
|
|
892
|
+
N * dx_hat
|
|
893
|
+
- backend.np.sum(dx_hat, axis=reduce_dims, keepdims=True)
|
|
896
894
|
- x_hat * backend.np.sum(dx_hat * x_hat, axis=reduce_dims, keepdims=True)
|
|
895
|
+
# - x_hat * backend.np.sum(dx_hat * self.x_hat if False else dx_hat * x_hat, axis=reduce_dims, keepdims=True)
|
|
897
896
|
)
|
|
898
|
-
|
|
899
897
|
x.grad += dx
|
|
900
|
-
|
|
898
|
+
|
|
901
899
|
out._backwards = _backward
|
|
902
|
-
|
|
903
900
|
return out
|
|
904
901
|
|
|
905
902
|
|
|
@@ -233,8 +233,10 @@ class Seq2Seq:
|
|
|
233
233
|
original_grad_state = not backend.NO_GRAD
|
|
234
234
|
needGradientHence(False)
|
|
235
235
|
try:
|
|
236
|
-
x_enc, enc_pad = self.encode(enc_idx)
|
|
236
|
+
x_enc, enc_pad = self.encode(enc_idx)
|
|
237
237
|
dec_idx = numpy.full((B, 1), sos_id, dtype=numpy.int64)
|
|
238
|
+
finished = numpy.zeros(B, dtype=bool) # NEW
|
|
239
|
+
|
|
238
240
|
for _ in range(max_new):
|
|
239
241
|
logits = to_cpu(self.decode_step(dec_idx, x_enc, enc_pad).data)[:, -1, :] / temperature
|
|
240
242
|
if top_k is not None:
|
|
@@ -243,9 +245,14 @@ class Seq2Seq:
|
|
|
243
245
|
z = logits - logits.max(-1, keepdims=True)
|
|
244
246
|
p = numpy.exp(z).astype(numpy.float64); p /= p.sum(-1, keepdims=True)
|
|
245
247
|
nxt = numpy.array([[numpy.random.choice(len(pr), p=pr)] for pr in p])
|
|
248
|
+
|
|
249
|
+
nxt[finished, 0] = self.pad_id # NEW: freeze finished rows
|
|
246
250
|
dec_idx = numpy.concatenate([dec_idx, nxt], axis=1)
|
|
247
|
-
|
|
248
|
-
|
|
251
|
+
|
|
252
|
+
if eos_id is not None:
|
|
253
|
+
finished |= (nxt[:, 0] == eos_id) # NEW: mark newly finished
|
|
254
|
+
if finished.all(): # NEW: stop when everyone's done
|
|
255
|
+
break
|
|
249
256
|
return dec_idx
|
|
250
257
|
finally:
|
|
251
258
|
if was_training:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from graphviz import Digraph
|
|
2
|
+
import numpy
|
|
2
3
|
|
|
3
4
|
def trace_graph(root_tensor):
|
|
4
5
|
"""Recursively traces the tensor graph to find all nodes and edges."""
|
|
@@ -14,25 +15,43 @@ def trace_graph(root_tensor):
|
|
|
14
15
|
build(root_tensor)
|
|
15
16
|
return nodes, edges
|
|
16
17
|
|
|
17
|
-
def
|
|
18
|
+
def draw_graph(root_tensor, max_char=50,format='svg', rankdir='LR'):
|
|
18
19
|
"""
|
|
19
20
|
Renders a computational graph starting from the given root_tensor.
|
|
20
21
|
format: output format (e.g., 'png', 'svg', 'pdf')
|
|
21
22
|
rankdir: 'LR' for left-to-right layout, 'TB' for top-to-bottom
|
|
22
23
|
"""
|
|
23
24
|
dot = Digraph(format=format, graph_attr={'rankdir': rankdir})
|
|
24
|
-
|
|
25
25
|
nodes, edges = trace_graph(root_tensor)
|
|
26
26
|
|
|
27
|
+
def format_array(arr):
|
|
28
|
+
"""Sanitizes and truncates array strings for safe Graphviz rendering."""
|
|
29
|
+
if arr is None:
|
|
30
|
+
return "None"
|
|
31
|
+
if isinstance(arr, numpy.ndarray):
|
|
32
|
+
s = numpy.array2string(arr, precision=4, suppress_small=True, separator=', ')
|
|
33
|
+
else:
|
|
34
|
+
s = str(arr)
|
|
35
|
+
|
|
36
|
+
s = s.replace('\n', '')
|
|
37
|
+
|
|
38
|
+
if len(s) > max_char:
|
|
39
|
+
s = s[:max_char-3] + "..."
|
|
40
|
+
s = s.replace('{', '\\{').replace('}', '\\}').replace('|', '\\|')
|
|
41
|
+
return s
|
|
42
|
+
|
|
27
43
|
for n in nodes:
|
|
28
44
|
uid = str(id(n))
|
|
29
45
|
shape_str = str(n.shape)
|
|
30
46
|
|
|
47
|
+
data_str = format_array(n.data)
|
|
48
|
+
grad_str = format_array(n.grad)
|
|
49
|
+
|
|
31
50
|
if n.op:
|
|
32
|
-
label = f"{{ {n.op} | shape: {shape_str} }}"
|
|
51
|
+
label = f"{{ {n.op} | shape: {shape_str} | data: {data_str} | grad: {grad_str} }}"
|
|
33
52
|
dot.node(name=uid, label=label, shape='record', style='filled', fillcolor='white')
|
|
34
53
|
else:
|
|
35
|
-
label = f"{{
|
|
54
|
+
label = f"{{ Node | shape: {shape_str} | data: {data_str} | grad: {grad_str} }}"
|
|
36
55
|
dot.node(name=uid, label=label, shape='record', style='filled', fillcolor='lightblue')
|
|
37
56
|
|
|
38
57
|
for child, parent in edges:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|