FastNN
A small deep learning library in Rust with optional CUDA kernels.
Tensors with reverse-mode autograd, the usual layers, transformers with
KV-cached generation, optimizers, data loading, resumable checkpoints and
safetensors. It runs on the CPU by default and on NVIDIA GPUs with the cuda
feature.
The codebase is small on purpose: 86 source files, about 12k lines, one idea
per file. You can follow loss.backward() from the training loop down to the
kernel. Every backward rule is checked against finite differences of its own
forward, and every CUDA kernel against the CPU path.
Features
| Area | What is in it |
|---|---|
| Tensors | Broadcasting arithmetic, matmul (batched, transposed forms), reductions, indexing, views, cat/stack/narrow, CPU or CUDA storage |
| Autograd | Reverse mode, no tape, no global state. no_grad, detect_anomaly, gradient clipping by norm or value |
| Layers | Linear, Conv1d, Conv2d (dilation, groups, depthwise), ConvTranspose1d/2d, pooling, BatchNorm2d, LayerNorm, RMSNorm, Dropout, Embedding, LSTM, GRU |
| Transformers | MultiHeadAttention with causal and padding masks, encoder and decoder stacks, encoder-decoder Transformer |
| Generation | KvCache, Sampler (temperature, top-k, top-p, repetition penalty), BeamSearch |
| Optimizers | SGD, Adam, AdamW, RMSprop, Adagrad, Adadelta, RAdam, Lion, plus Lookahead and Ema wrappers |
| Schedules | Warmup, CosineAnnealing, OneCycle, StepDecay, Constant |
| Losses | Cross-entropy (label smoothing, class weights, ignore index), MSE, MAE, BCE, Huber, KL, NLL, focal, dice, Gaussian and Poisson NLL, triplet, contrastive, InfoNCE |
| Data | Dataset trait, shuffling DataLoader with device placement, MNIST |
| Checkpoints | Weights, full training state (weights, optimizer, step), safetensors read and write with F16/BF16 widening |
Installation
Needs Rust 1.87 or newer. With --features cuda you also need the CUDA toolkit,
11.8 or newer, for GPUs from Turing to Hopper (compute capability 7.5 to 9.0).
Quick start
An MNIST training loop:
use ;
use *;
To train on a GPU, move the model and the loader:
let device = best; // GPU if there is one, else CPU
model.to_device;
let loader = new.to_device;
Examples
Use --release. Debug builds are about 50x slower.
| Example | What it shows |
|---|---|
simple_mlp |
MLP learning XOR, the smallest training loop |
mnist_mlp |
Dataset download, batching, evaluation, checkpointing |
mnist_cnn |
Convolutions, batch norm, pooling, an LR schedule |
char_lm |
Small GPT trained from scratch, resumes after Ctrl-C |
finetune |
Pretrain, freeze the backbone, train a new head |
gan |
Two networks trained against each other |
vae |
Variational autoencoder on 2-D data |
reinforce |
Policy gradient on a bandit |
multi_task |
Shared trunk, two heads, one combined loss |
char_lm uses a small embedded corpus by default, or any text file:
Want to see it without writing any code? fastnn-visualizer trains an MLP on MNIST with FastNN and lets you draw a digit in the browser and watch the network light up as it predicts.
Prebuilt binaries
Each release has the examples prebuilt, so you can try them without installing Rust.
| Platform | Archive |
|---|---|
| Linux x86_64 | fastnn-<version>-x86_64-linux.tar.gz |
| Linux ARM64 | fastnn-<version>-aarch64-linux.tar.gz |
| Linux x86_64 with CUDA | fastnn-<version>-x86_64-linux-cuda.tar.gz |
| macOS Apple Silicon | fastnn-<version>-aarch64-macos.tar.gz |
| macOS Intel | fastnn-<version>-x86_64-macos.tar.gz |
| Windows x86_64 | fastnn-<version>-x86_64-windows.zip |
Releases include a SHA256SUMS file and a build provenance attestation for each
archive (gh attestation verify <archive> --repo CanReader/FastNN). The CUDA
build needs an NVIDIA driver new enough for the toolkit it was built with.
Platform support
| Platform | CPU | CUDA |
|---|---|---|
| Linux x86_64 | Tested in CI | Built in CI, parity tested on real hardware |
| Linux ARM64 | Built for releases | Not supported |
| macOS (Intel, Apple Silicon) | Tested in CI | Not available |
| Windows x86_64 | Tested in CI | Should build, not tested |
How it fits together
data Dataset ─► DataLoader ─► Batch
│
nn Module ──► forward ────────┤ layers hold Param handles
▼
tensor Tensor ops on CPU or CUDA ─┴─► loss
│
autograd loss.backward() walks the graph the ops built,
depositing gradients in each Param's slot
│
optim opt.step() reads those slots and updates in place
Everything starts on the CPU. model.to_device(device) and
DataLoader::to_device move things over, and mixing devices panics instead of
copying behind your back.
The graph is the tensors. A tensor produced by a differentiable op carries a
node naming the rule that made it and the tensors it consumed, and
loss.backward() walks that. Drop the loss and the graph frees itself. A tensor
with no node is plain data, so there is no requires_grad flag. Wrap inference
in no_grad(|| ...) to skip building a graph.
Param is a handle, not a value. The model and the optimizer hold handles to
the same weight and gradient, so opt.step() doesn't borrow the model. Weight
tying works the same way: use the handle twice and the gradients add up.
Long training runs
Resume. save() writes weights, which is enough for a finished model.
Resuming also needs the optimizer's moments and step count. Without them Adam
restarts at step 1 and the first update is far too large.
let mut step = load_training.unwrap_or;
while step < total
Loading a plain checkpoint with load_training is an error, not a silent
optimizer reset.
NaNs. One non-finite gradient poisons the weights, and by the time the loss
prints NaN the culprit is long gone. detect_anomaly panics at the op that
produced it.
detect_anomaly;
if !opt.gradients_are_finite // cheap guard: skip the batch
Both read every gradient, so don't leave them on in a healthy loop.
Freezing. A frozen parameter hands out a detached tensor, so the backward pass stops there and no gradient is computed for it.
backbone.freeze;
let mut opt = new; // updates only the head
Fallible ops. Ordinary ops panic on bad shapes. Where a shape comes from a
config file or an upload, use the try_ variants.
let y = x.try_matmul?; // Error::Shape, not a panic
x.try_reshape?;
table.try_index_select?;
They validate and then call the panicking op, so there is one implementation of each operation.
Extending
A layer. Implement forward, plus named_parameters if it has weights.
Device placement, parameter counting and gradient zeroing follow from those.
use scoped;
An op. Forward in tensor/ops/, backward rule in autograd/ops/, attach it
with with_grad, add a case to tests/gradcheck.rs.
// tensor/ops/unary.rs
// autograd/ops/unary.rs
The rule returns gradients in input order. Save detached tensors, otherwise the saved value pins the graph that produced it. CONTRIBUTING.md has the full checklist.
A dataset. Implement Dataset and pass it to a DataLoader. Items are
written into caller-provided slices instead of returned as tensors, so batching
60,000 images doesn't allocate 60,000 throwaway ones.
Errors
Shape and device mistakes panic. They are bugs in the calling code, like
indexing past the end of a slice. Result is for things that fail at runtime:
let device = cuda?; // no GPU
let data = load?; // download or parse failed
load?; // missing file, or a shape that moved
Testing
cuda_parity runs each op on both devices and compares. It skips itself when
there is no GPU. It has already caught a real bug: the softmax kernel's block
reduction assumed a power-of-two thread count and dropped the tail of every row
whose width wasn't one, which included MNIST's ten classes.
CI runs formatting, clippy, the suite on Linux, macOS and Windows, an MSRV build, docs, packaging and a CUDA compile. A nightly job reruns the suite in release on stable and beta and trains the examples that don't need a download.
CUDA
The cuda feature is opt-in. With it on, build.rs compiles cuda/kernels.cu
with nvcc and links cudart, cublas and curand.
nvccis found onPATH, or set theNVCCenvironment variable.- Headers and libraries come from
CUDA_PATH, thenCUDA_HOME, then the default install location. Make sure that is the same toolkit as yournvcc. - If
nvccrejects your system compiler as too new, pointFASTNN_NVCC_CCBINat one it accepts. - Without the feature,
cuda/stubs.csupplies the symbols,Device::cuda(0)returns an error, and everything runs on the CPU.
Matmul goes through cuBLAS. The transposed forms the backward pass needs
(matmul_nt, matmul_tn) use a transpose flag instead of a transposed copy.
GPU allocations come from a size-keyed free list, so the short-lived temporaries
in a training step don't each hit the driver.
Project layout
src/
tensor/ the array type and everything you can do to it
core.rs Tensor: shape, storage, graph link
checked.rs try_* variants that report instead of panicking
shape.rs strides, broadcasting, index math
storage.rs the bytes, on one device or the other
device.rs Device::cuda(0) -> Result
init.rs zeros, randn, kaiming, xavier, ...
ops/ arith unary activation matmul reduce view
index conv pool norm
autograd/ reverse-mode differentiation
node.rs Backward trait, graph nodes, gradient slots
engine.rs the reverse pass
mode.rs no_grad
anomaly.rs detect_anomaly
ops/ one backward rule per forward op, same file names
nn/ layers, all implementing Module
linear conv conv1d conv_transpose pooling norm activation
dropout embedding shape attention transformer decoder rnn
cache (kv cache) sample (sampling) beam (beam search)
loss metric_losses module param sequential
optim/ sgd adam rmsprop adagrad radam lion lookahead
ema schedule state
data/ dataset loader mnist
serialize/ checkpoint training (weights + optimizer)
safetensors half (F16/BF16 conversion)
cuda/ ffi (raw bindings) kernels (safe wrappers) buffer
cuda/kernels.cu every GPU kernel
cuda/stubs.c CPU-only stand-ins for the same symbols
tensor/ops/ and autograd/ops/ mirror each other file for file. The forward
for relu is in tensor/ops/activation.rs, its derivative in
autograd/ops/activation.rs.
Limitations
- Version 0.x, so the API can still change between minor releases.
- Compute is
f32. F16 and BF16 checkpoints load and are widened on read. LSTMandGRUare built from ordinary ops, one graph node per gate per timestep. Fine for short sequences, use a transformer for long ones.- Tensors are always contiguous.
permuteandexpandwrite a new buffer instead of returning a strided view.
Contributing
See CONTRIBUTING.md. Issues labelled
good first issue
are a good place to start. Report security problems privately, see
SECURITY.md.
If FastNN is useful to you, a star helps other people find it.