A fast C++ / CUDA engine for training neural networks with predictive coding — a brain-inspired alternative to backpropagation — with a simple Python interface.
Website & Documentation • Quick Start • Benchmarks • Contributing
At a glance: Deepity's
SimplePCNreaches 98.21% MNIST test accuracy in 487.5s on a laptop CPU — faster than the two established predictive-coding libraries, jpc (JAX) and ngc-learn, at matching or better accuracy on an identical setup. Details, and where Deepity does not win, are in Performance & Benchmarks.
- What is Deepity?
- New to predictive coding? Start here
- Why use predictive coding?
- Quick Start
- Which network should I use?
- Performance & Benchmarks
- Building from source
- Architecture & Roadmap
- Contributing • License
Most neural networks today (anything you'd build in PyTorch or TensorFlow) learn with backpropagation: the network makes a prediction, measures how wrong it is at the output, and sends that error backward through every layer in one big pass.
Predictive Coding Networks (PCNs) learn differently. Each layer learns from a local signal — how far its own activity is from what the layer next to it predicted — instead of waiting for one global error to flow back from the output. The idea comes from neuroscience, where it's a leading theory of how the brain learns.
Deepity is an engine for building and training these networks quickly:
- A C++ core with hand-optimized CPU kernels (SIMD, Intel MKL or OpenBLAS) and a CUDA backend for NVIDIA GPUs.
- A Python package (
pip install pydeepity,import deepity) so you can define and train networks in a few lines, without writing any C++. - Several PCN variants, from a plain beginner-friendly network to one that bundles recent research techniques you can switch on one at a time.
You don't need to know anything about predictive coding to try it. The next section explains the core idea in plain terms.
Think of each layer of the network as holding a guess about what it's seeing. Every layer also makes a prediction about what the layer next to it should be seeing. Wherever a prediction doesn't match the guess, that mismatch is a prediction error.
Training a PCN on one batch of data works in two phases:
- Settling (inference). The input is fixed at the bottom of the network and the correct answer at the top. Then every layer repeatedly nudges its guess to reduce the prediction errors around it. Each round of nudging is one settling step, and the number of rounds is called T. After enough steps, the guesses stop changing much: the network has "settled" on a consistent explanation of the input and the answer.
- Learning. Each layer then adjusts its weights to make its predictions closer to the settled guesses. This update uses only that layer's own prediction error. No layer needs to know what's happening elsewhere in the network.
The total amount of prediction error across the network is called its energy. Settling lowers the energy for the current batch, and learning lowers it for future batches. When you call train_step in Deepity, the number it returns is this energy.
The main trade-off to know about: more settling steps (a larger T) give a better-converged update, but cost proportionally more compute per batch. Most of the speed and accuracy differences in the benchmarks below come down to T.
Deepity implements two ways of turning prediction errors into weight updates:
- Classic settling (
SimplePCN,FullPCN, and most other variants): every layer's guess is refined over several settling steps, as described above, before the weights update. - Direct Kolen-Pollack, or DKP (
DKPPCN): trains a separate set of feedback weights alongside the normal forward weights, instead of reusing the forward weights to send predictions back down. In practice it needs far fewer settling steps:DKPPCNbelow uses 2 whereSimplePCNuses 20, which is most of why it's dramatically faster, at a real accuracy cost (see the MNIST comparison).
- No stored backward graph. Backpropagation has to keep the entire forward computation in memory until the backward pass runs. In a PCN, each layer's update depends only on its own local prediction error, so Deepity never stores a backward computation graph. (O(1) memory footprint for the learning signal.)
- No weight-transport requirement. In backpropagation, each layer's backward pass must use the exact transpose of its forward weights, which is one of the main reasons it's considered biologically implausible. Deepity's local updates don't inherit that constraint, and DKP relaxes it further by learning the feedback pathway instead of assuming it.
- A good fit for future hardware. That same locality is why the research literature is interested in predictive coding for neuromorphic and edge hardware. Deepity doesn't have benchmarks on either yet, so treat this as context rather than a claim about this engine.
From PyPI (prebuilt wheels, Linux + Windows, Python 3.9-3.13):
uv add pydeepity
# or: pip install pydeepityThis is an early pre-release (0.1.0a1) -- the CPU engine itself is
well-tested (see Performance & Benchmarks
below), but these wheels are CPU-only and some edge cases are still being
worked through; see Architecture & Roadmap.
From source (needed for CUDA, or to track the latest dev/CUDA work):
you'll need CMake 3.21+, Ninja, a C++20 compiler, OpenBLAS or Intel MKL, and
Python 3.9+ with nanobind (the full list is in Building from
source). Then:
git clone https://github.com/Ra4ster/Deepity.git
cd Deepity
python build.py Release OpenBLAS --fastThis builds the C++ engine, runs the test suite, and builds the deepity Python package.
A tiny network with 2 inputs, 8 hidden units and 1 output, trained on a single example:
import numpy as np
from deepity import SimplePCN, Linear, TanH
# Declare the layers, the same way you would in PyTorch.
net = SimplePCN(
Linear(2, 8), TanH(),
Linear(8, 1),
batch_size=1,
)
# Set how fast the weights learn (learning_rate) and how fast
# each layer's guess moves during settling (inference_rate).
net.configure(learning_rate=0.05, inference_rate=0.3, lmbda=0.0001, optimizer="SGD")
x = np.array([1.0, -1.0], dtype=np.float32) # input
y = np.array([1.0], dtype=np.float32) # target
# Settle for 50 steps, then update the weights locally.
# The return value is the network's energy (total prediction error).
energy = net.train_step(x, y, steps=50)Run train_step in a loop over your data and the energy should fall as the network learns.
- A full training script:
examples/mnist.pytrains on MNIST end to end. It's the script behind the headline benchmark. - Larger experiments:
research/imagenet.pyandresearch/imagenet_pc_vgg7_ce.pytrain convolutional PCNs on ImageNet-scale data. - Using C++ directly: start with
include/deepity/networks/DiscriminativePCNetwork.h. Mostdeepityclasses correspond directly to a C++ network class underinclude/deepity/networks/.
| If you want... | Use | Notes |
|---|---|---|
| The simplest, most standard predictive coding | SimplePCN |
Classic settling. The best place to start, and the network behind the headline MNIST result. |
| Much faster training, accepting some accuracy loss | DKPPCN |
Direct Kolen-Pollack feedback, so very few settling steps are needed (2 instead of 20 in the benchmarks). |
| Recent research techniques, switchable one at a time | FullPCN |
Includes DKP and adds optional extras such as µPC scaling, residual connections, incremental PC (iPC), error-based PC (ePC), momentum settling and a cross-entropy output. Everything is off by default. |
| Image data | The convolutional variants | Convolutional PCNs, used in the ImageNet research scripts. |
How to read this section: the headline comparison pits Deepity against other predictive-coding libraries running exactly the same network, data and training settings, so the only difference is the implementation. A standard PyTorch backpropagation run is included as a reference point.
Development and testing for the CUDA backend happens on the Ohio Supercomputer Center (Project PAS0350), on NVIDIA A100 GPUs. The numbers below are all CPU-only, run on one machine (12th Gen Intel Core i7-12700H, 20 logical threads, native + MKL build), so they measure the engine's single-machine CPU path specifically, not the CUDA backend. Every command below is reproducible from the repo root, and raw output for each run is committed under logs/.
MNIST is a standard dataset of handwritten digits (0–9); the task is to classify each image.
The network is 784→512→512→10 (sigmoid), with T=20 settling steps/batch, batch size 250, 100 epochs, and AdamW lr=0.00373 with 0.94/epoch decay — identical across the first two columns so the comparison is apples-to-apples.
Deepity SimplePCN |
jpc (JAX) | ngc-learn pc_discrim |
PyTorch backprop (control) | |
|---|---|---|---|---|
| Test / dev accuracy | 98.21% | 97.55% | 98% (best dev) | 97.51% |
| Wall-clock, 100 epochs | 487.5s | 531.4s | 1707.7s | 627.8s |
| Command | python examples/mnist.py 7 100 20 |
python experiments/benchmark_jpc_mnist.py 7 100 |
ngc-museum's pc_discrim exhibit, unmodified |
python examples/test_ffnn_deep.py 7 100 |
| Raw log | logs/mnist_simd_fusion_confirm.txt |
logs/jpc_mnist_run.txt |
logs/ngclearn_pc_discrim_mnist.txt |
logs/bp_control_mnist.txt |
- Deepity vs. jpc is the closest comparison: both are predictive-coding implementations on the same architecture, settling-step count, batch size, epoch count and optimizer schedule. jpc is built on JAX/XLA; Deepity is hand-tuned C++. Deepity trains faster at matching accuracy.
- Deepity vs. ngc-learn: identical architecture, settling-step count, batch size, epoch count, forward-projection init and optimizer schedule, run as two independent predictive-coding implementations on the same machine. Deepity reaches equal-or-better accuracy in less time.
- The PyTorch column is a reference point, not a PC run. It's single-pass backprop (one forward and one backward pass per batch) against the same targets via MSE loss. It does one pass per batch where PC does T=20 settling passes, so it isn't directly comparable to the PC columns.
The faster option: DKPPCN (784→512→10, batch size 1000, only 2 inference steps/batch instead of 20) reaches 95.97% in 49.6s on the same CPU (python examples/mnist_dkppcn.py 7 50 2 cpu, logs/DKPPCN_native_mkl.txt). That's a different point on the speed/accuracy trade-off curve.
What this measures: GFLOPS is billions of floating-point operations per second, a standard measure of how efficiently code uses the processor. Higher means more of the hardware's raw compute is doing useful work.
tests/tGflopsBenchmark.cpp, built as the GflopsBenchmark target via Google Benchmark, measures sustained GFLOPS for a full PC train step (SimplePCN, 784-512-256-64-10, batch 256, T=20):
python build.py Release OpenBLAS --fast
./build/Release/bin/GflopsBenchmarkHow the FLOPs are counted (from the file's own header comment): for a layer transitioning UpdateWeights() runs a third, equally sized GEMM once per train step (not once per settling step) for the weight gradient. Summed over every weighted layer transition:
Plugged in for the benchmarked network (
| Layer transition | size × nextSize |
|---|---|
| 784 → 512 | 401,408 |
| 512 → 256 | 131,072 |
| 256 → 64 | 16,384 |
| 64 → 10 | 640 |
| Σ (size × nextSize) | 549,504 |
That 11.54 GFLOP figure is fixed by the architecture, batch size and step count. What's measured is the wall time per train step; Google Benchmark's FLOPS counter divides the fixed number above by that measured time:
See logs/gflops_benchmark.txt for the raw runs.
The same FLOP-accounting formula applied to the full MNIST, matched configuration run — 329 TFLOP total across 100 epochs (24,000 train steps), at 487.5s wall-clock — gives 675 GFLOPS/s sustained, end to end.
What this measures: activation functions (tanh, sigmoid, ReLU, GELU) are applied to every value in every layer at every settling step, so their speed matters. Deepity uses hand-written SIMD kernels, which process many values per CPU instruction, instead of plain one-value-at-a-time loops.
tests/tActivations.cpp, built as the ActivationBenchmark target, compares Deepity's SIMD kernels (hand-written AVX2/AVX-512/SSE intrinsics calling SLEEF for the transcendental ones; see include/deepity/utils/activations/) against naive std::tanh/std::exp-based scalar loops at matched input sizes:
./build/Release/bin/ActivationBenchmarkThroughput in millions of elements/second; speedup = Deepity ÷ naive; raw output in logs/activation_benchmark.txt:
Tanh
| Size | 256 | 512 | 784 | 16,384 | 131,072 |
|---|---|---|---|---|---|
| Deepity (SIMD) | 303.6 | 394.8 | 460.7 | 559.8 | 3044.0 |
std::tanh (naive) |
223.7 | 243.2 | 250.8 | 258.6 | 261.2 |
| Speedup | 1.36x | 1.62x | 1.84x | 2.16x | 11.66x |
Sigmoid
| Size | 256 | 512 | 784 | 16,384 | 131,072 |
|---|---|---|---|---|---|
| Deepity (SIMD) | 892.8 | 1279.6 | 1421.4 | 1989.7 | 2091.5 |
naive (1/(1+exp(-x))) |
358.1 | 416.9 | 457.3 | 505.2 | 506.2 |
| Speedup | 2.49x | 3.07x | 3.11x | 3.94x | 4.13x |
ReLU
| Size | 256 | 512 | 784 | 16,384 | 131,072 |
|---|---|---|---|---|---|
| Deepity (SIMD) | 1323.5 | 2643.1 | 4031.7 | 18520.3 | 16724.8 |
naive (max(0,x)) |
1504.4 | 2917.8 | 4102.4 | 19042.4 | 16681.0 |
| Speedup | 0.88x | 0.91x | 0.98x | 0.97x | 1.00x |
GELU
| Size | 256 | 512 | 784 | 16,384 | 131,072 |
|---|---|---|---|---|---|
| Deepity (SIMD) | 357.2 | 416.6 | 431.5 | 462.3 | 456.4 |
| naive (erf-based) | 153.4 | 162.6 | 169.6 | 179.7 | 166.1 |
| Speedup | 2.33x | 2.56x | 2.54x | 2.57x | 2.75x |
Tanh, Sigmoid and GELU win at every tested size, with the margin generally growing with size. Tanh's jump at the largest size (131,072) is because Tanh.h's settling loop also crosses an OpenMP threshold (n > 16384) at that size, adding multi-core parallelism on top of SIMD width. ReLU sits at 0.88x–1.00x, essentially tied with the naive loop. That's expected: relu(x) = max(0, x) is one comparison with no multiply, so it's limited by memory bandwidth rather than compute, and the compiler's own auto-vectorization of a one-line loop already reaches the same ceiling.
Note: the CUDA (GPU) backend is a separate code path with its own characteristics — TF32-enabled im2col+cuBLAS convolution, and CUDA graph capture for the settling loop — and isn't covered by the CPU numbers above. ImageNet-scale training on it is in active development; see research/imagenet_pc_vgg7_ce.py, which supports checkpoint/resume and a command-line-overridable settling-step count for sweeping T.
- CMake 3.21+ and Ninja
- A C++20 compiler (Clang recommended; GCC and MSVC are also supported)
- OpenBLAS or Intel MKL
- Python 3.9+ with development headers, plus
nanobind(for thedeepitybindings) - CUDA Toolkit (optional: the CUDA backend is detected automatically if present; otherwise the project builds and runs CPU-only)
See CONTRIBUTING.md for the full prerequisite list and known-working manual CMake invocations (for example, Windows + Clang).
build.py handles CMake/Ninja configuration, compilation and running the test suite in one command:
git clone https://github.com/Ra4ster/Deepity.git
cd Deepity
python build.py Release OpenBLAS --fastRelease/Debugselects the build type.OpenBLAS/MKLselects the linear-algebra library.--fasttargets a portable AVX2/FMA baseline. Runpython build.py --list-profilesfor the other options, andpython build.py --helpfor CUDA, test and job-count flags.
The deepity Python package is built too, unless you pass --no-python-bindings.
Deepity started as an academic research project and is now becoming a scalable infrastructure tool.
Done
- Fused, optimized CUDA kernels
- DKP-PC acceleration
- ImageNet data loading pipeline (see
research/imagenet.py) - Cross-platform CI/CD (Ubuntu + Windows wheel builds and pyright type checks on every push; see
.github/workflows) - Checkpoint/resume for long training runs:
ModelIO(safetensors-backed) saves a checkpoint after every epoch, and rerunning the same command resumes from<prefix>.safetensors+<prefix>_meta.json, carrying overglobal_step,best_accand the data-shuffling RNG state, so the learning-rate schedule and data order pick up exactly where they left off
In progress / planned
- Stable, large-scale ImageNet training (actively being tuned)
- cuDNN-backed convolution (Winograd / implicit-GEMM), replacing the current im2col + cuBLAS-GEMM path in
CUDABackendConv.cu. TF32 is already on by default, but im2col+GEMM still trails cuDNN's specialized small-kernel (e.g. 3x3) convolution algorithms. - Fewer settling steps (smaller T) for genuine multi-step PC. T is currently taken directly from PCX (a JAX-based predictive-coding research library, used here as an external reference point) and its per-architecture recipes, e.g. T=12 for VGG-7. It's now a command-line argument on
research/imagenet_pc_vgg7_ce.pyfor sweeping, but whether a smaller T reaches comparable accuracy for proportionally less compute per batch is still untested.
We're actively looking for contributors to help build out the auxiliary systems. If you have experience with C++/CUDA performance work or Python (build systems/DevOps), check out the open issues marked good first issue or reach out directly.
MIT License @ 2026
