Skip to main content

A custom autograd engine and Transformer block built from scratch.

Project description

cogforge

A from-scratch deep learning library built on nothing but NumPy — a reverse-mode autograd engine extended all the way to a working GPT.

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 attention, and a decoder-only transformer (GPTV1) you can actually train and sample from.

There is no C++, no CUDA, no PyTorch — just NumPy and explicit, hand-derived gradients. The goal is to understand every gradient that flows, not to be fast.


Table of contents


Installation

pip install cogforge-engine

Requires Python 3.8+ and NumPy. That's the only dependency.

The package is organized into two modules:

Module Contains
cogforge.app The autograd engine (Tensor) and every building block — layers, optimizers, losses, normalization, attention.
cogforge.models Ready-to-use models. Currently GPTV1.
from cogforge.app import Tensor, Linear, Adam, MultiHeadAttention   # building blocks
from cogforge.models import GPTV1                                    # models

Quick start

import numpy as np
from cogforge.app import Tensor

# Build a graph
a = Tensor(np.array([2.0, 3.0]))
b = Tensor(np.array([4.0, 5.0]))
c = (a * b).sigmoid().softmax()

# Backpropagate (note the spelling: backwards, with an 's')
c.backwards()

print(a.grad)   # gradient of the output w.r.t. a

Every Tensor carries a .data (the NumPy array), a .grad (same shape, accumulates gradients), and a hidden _backwards closure that knows how to push gradient to its parents. Calling .backwards() on any node runs a topological sort and walks the graph in reverse.


Core concept: the Tensor

Tensor(array, children=(), requires_grad=True, typed="compressed")
Argument Meaning
array Any array-like; stored as a NumPy array in .data.
children Parent tensors in the graph (set internally by ops; you rarely pass this).
requires_grad Reserved flag (currently informational).
typed "compressed"float32 (default), anything else → float64.

Gradients accumulate into .grad. Always zero them between optimization steps (the optimizers do this for you via zero_grad()).


API reference

Tensor — autograd engine

Differentiable operations (each builds graph and defines its own backward):

Operation Notes
a + b, a - b, a * b Elementwise, with broadcasting support.
a @ b Batched matmul; gradients are correctly un-broadcast.
a[key] Indexing/slicing.
.relu()
.sigmoid()
.tanh()
.softmax(axis=-1) Numerically stable (max-subtraction).
.view(shape) Reshape (handles non-contiguous data).
.flatten() Flattens everything after the batch dim → (B, -1).
.flatten_consective(num) Groups num consecutive timesteps. Expects a 3-D (B, T, C) tensor; T must be divisible by num.
.transpose(axes) Permute axes (pass the full permutation tuple).
.masked_fill(mask, value) Sets entries where mask is True to value (used for causal attention).

Backward pass

Method Notes
.backwards() Primary. Iterative topological sort — safe for deep/long graphs.
.backwards_recursive() Legacy recursive version; can hit Python's recursion limit on long sequences. Prefer .backwards().

Static helper

  • Tensor.unbroadcast(grad, shape) — reduces a broadcasted gradient back to the original parameter shape. Used internally.

Losses

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.

Loss Input expectation Use when
Tensor.cross_entropy_loss(predictions, targets) predictions are probabilities (call .softmax() first), targets one-hot. You already have a softmax in your graph.
Tensor.softmax_cross_entropy(scores, targets) scores are raw logits, targets one-hot. Softmax is fused inside (stable). Works for 2-D (B,V) and 3-D (B,T,V). Standard classification / LM. Recommended.
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.
Tensor.cross_entropy_loss_masked(predictions, targets, mask) Probabilities + per-row mask (1 = real, 0 = pad). Padded batches.
Tensor.softmax_cross_entropy_masked(scores, targets, mask) Logits + per-row mask. Padded batches, fused softmax.

ℹ️ softmax_cross_entropy and sparse_softmax_cross_entropy apply softmax internally. Do not pass already-softmaxed values into them. cross_entropy_loss is the opposite — it expects probabilities.


Layers

Linear(nin, nout)

Affine transform x @ W + b. He-initialized weights. .parameters()[W, b].

Embedding(vocab_size, embedding_dim)

Lookup table. Call with an integer index array; backward scatters gradients correctly (uses np.add.at, so repeated indices accumulate). .parameters()[weights].

LayerNorm(dim, eps=1e-5)

Normalizes over the last dimension. Learnable gamma/beta. .parameters()[gamma, beta].

BatchNorm1D(dim, eps=1e-5, momentum=0.1)

Normalizes over the batch (and time, for 3-D input). Tracks running_mean/running_var for inference. Toggle .training = True/False. Learnable gamma/beta.

Attention(dk)

Scaled dot-product attention. Call attention(Q, K, V, mask=None). dk is the key dimension (sets the 1/√dk scale).

MultiHeadAttention(dinp, dmodel, dout, n)

n heads, dmodel split into n chunks of size dmodel // n (must divide evenly). Projects input dinp → dmodel, attends, projects dmodel → dout. Call mha(query, key, value, mask=None). .parameters() returns all four projection layers' params.

FeedForward(dmodel, dff=None)

Position-wise MLP: Linear → ReLU → Linear. dff defaults to 4 * dmodel. .parameters() included.

Transformer(dmodel, n, dff=None)

A pre-norm decoder block: x + Attn(LN(x)) then x + FF(LN(x)). n = number of attention heads. Call block(x, mask=None). .parameters() included.

PositionalEncoding(max_len, dmodel)

Fixed sinusoidal positions, added to the input. Call pe(x). No parameters.


Containers

Sequential(layers)

Runs layers in order. .train() / .test() flip the training flag on any layer that has one (e.g. BatchNorm1D).

⚠️ Sequential.parameters() only collects layers exposing W, b, gamma, or beta attributes (i.e. Linear, LayerNorm, BatchNorm1D). Composite layers like MultiHeadAttention, FeedForward, and Transformer hold sub-modules, so their parameters are not picked up here — gather those via each module's own .parameters().

MLP(layer_sizes)

Convenience feed-forward net: Linear → ReLU between layers, plain Linear output. Built from a list of sizes, e.g. MLP([784, 128, 64, 10]).

  • .save(filename="best_model.npz") / .load(filename="best_model.npz") — persist/restore weights.
  • Note: MLP does not expose a parameters() method; collect them via [p for layer in mlp.layers for p in layer.parameters()] if you want to optimize it.

Recurrent

RNNCell(input_dim, hidden_dim)

One tanh recurrence step: h_next = tanh(i2h(x) + h2h(h_prev)). .parameters() included.

RNN(input_dim, hidden_dim)

Unrolls a cell over a list of timestep tensors (each (B, input_dim)) and returns the list of hidden states (each (B, hidden_dim)). Optional prev_hidden.

StackedRNN(input_dim, hidden_dim, num_layers)

Multiple RNN layers stacked. Returns (top_layer_states, per_layer_final_states) — the second value is convenient for seq2seq.


Sequence-to-sequence

Bridge(enc_hidden, dec_hidden, enc_layers, dec_layers, mode="project")

Maps encoder final hidden states to decoder initial hidden states, handling mismatched layer counts and hidden sizes.

mode Behavior
"project" One learned Linear(enc_hidden → dec_hidden) per decoder layer. General, recommended.
"tie" No parameters; requires enc_hidden == dec_hidden. Selects/repeats raw states.

Optimizers

Both take an iterable of parameter tensors and share the same interface: step(), zero_grad(), clip_grads(max_norm=5.0).

SGD(parameters, learning_rate=0.01)

Plain stochastic gradient descent.

Adam(parameters, lr=1e-3, beta1=0.9, beta2=0.999, eps=1e-8)

Adam with bias correction. Recommended for transformers.

opt = Adam(model.parameters(), lr=3e-4)
opt.zero_grad()
loss.backwards()
opt.clip_grads(1.0)   # optional gradient clipping
opt.step()

Models

GPTV1(vocab, d_model, n_heads, n_layers, max_len, d_ff=None)

A decoder-only transformer (token embedding + sinusoidal positions + stacked pre-norm Transformer blocks + final LayerNorm + output head). Causal masking is applied internally.

Method Description
model(idx) idx: integer array (B, T). Returns logits (B, T, vocab).
model.parameters() All trainable tensors.
model.generate(idx, n_new, temperature=1.0, top_k=None) Autoregressive sampling. Crops to max_len, supports temperature and top-k. Returns (B, T + n_new).

Worked example: train a char-level GPT

import numpy as np
from cogforge.app import Tensor, Adam
from cogforge.models import GPTV1

# --- data -------------------------------------------------------------
text  = open("input.txt").read()
chars = sorted(set(text))
stoi  = {c: i for i, c in enumerate(chars)}
itos  = {i: c for i, c in enumerate(chars)}
data  = np.array([stoi[c] for c in text])
vocab = len(chars)

# --- model ------------------------------------------------------------
block = 64
model = GPTV1(vocab=vocab, d_model=128, n_heads=4,
              n_layers=4, max_len=block)
opt   = Adam(model.parameters(), lr=3e-4)

def get_batch(bs=32):
    ix = np.random.randint(0, len(data) - block - 1, size=bs)
    x  = np.stack([data[i:i + block]     for i in ix])
    y  = np.stack([data[i + 1:i + block + 1] for i in ix])
    return x, y

# --- train ------------------------------------------------------------
for step in range(2000):
    x, y   = get_batch()
    logits = model(x)                                  # (B, T, vocab)
    loss   = Tensor.sparse_softmax_cross_entropy(logits, y)

    opt.zero_grad()
    loss.backwards()
    opt.clip_grads(1.0)
    opt.step()

    if step % 100 == 0:
        print(f"step {step:4d} | loss {loss.data:.4f}")

# --- sample -----------------------------------------------------------
ctx = np.array([[stoi["\n"]]])
out = model.generate(ctx, n_new=300, temperature=0.8, top_k=20)
print("".join(itos[i] for i in out[0]))

Gotchas

  • It's backwards(), not backward(). The backward pass method has a trailing s.
  • 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.
  • Gradients accumulate. Call optimizer.zero_grad() every step (or p.grad[...] = 0), or gradients pile up across iterations.
  • Sequential.parameters() is shallow — see the note under Containers. For attention/feed-forward/transformer stacks, gather parameters through each module's own .parameters() (as GPTV1.parameters() does).
  • RNNs operate on lists, not a single (B, T, C) tensor — pass a list of per-timestep tensors.

Roadmap

Planned / under consideration:

  • RoPE (rotary position embeddings) with length interpolation
  • SwiGLU feed-forward and RMSNorm
  • Weight tying between embedding and output head
  • KV cache for faster generation
  • Linear-attention block (as a study in the recall-vs-cost tradeoff)

License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cogforge_engine-1.0.0.tar.gz (19.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cogforge_engine-1.0.0-py3-none-any.whl (20.4 kB view details)

Uploaded Python 3

File details

Details for the file cogforge_engine-1.0.0.tar.gz.

File metadata

  • Download URL: cogforge_engine-1.0.0.tar.gz
  • Upload date:
  • Size: 19.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cogforge_engine-1.0.0.tar.gz
Algorithm Hash digest
SHA256 17e1b42e3249fee676e73ed2d26e44e48f0311bba20c642f958c8e51838f354c
MD5 997c0f510722e7080490828bbe32887a
BLAKE2b-256 939b5e3f519c13344d847c18c930ccd74d053109274d2d95f2268fda64563379

See more details on using hashes here.

Provenance

The following attestation bundles were made for cogforge_engine-1.0.0.tar.gz:

Publisher: release.yml on avikmjd2/cogforge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cogforge_engine-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for cogforge_engine-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 04e82d03a78ebf819cacc9188926581a25efe24cbcf57aedfddc7ce2814c166a
MD5 eefa205c0875cf3bb27ba01a4863686f
BLAKE2b-256 0f5c051905245b892a8df7a35a0d42ef3ce7cf31f3b1d07c77f35af9c27191d3

See more details on using hashes here.

Provenance

The following attestation bundles were made for cogforge_engine-1.0.0-py3-none-any.whl:

Publisher: release.yml on avikmjd2/cogforge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page