Skip to content

Repository files navigation

Conditional VAE in C : No PyTorch/JAX

A ground-up implementation of a Conditional Variational Autoencoder (CVAE) in C, trained on MNIST digits, with zero external dependencies beyond libc and libm.

Author: Ashwin Shirke

Digits 0 & 1 (v1): Digits 0 and 1


Motivation

This project is a deliberate exercise in understanding a generative model at its lowest level.

The starting point: can I implement a VAE — the math, the training loop, the backpropagation — without hiding anything behind a library?

Writing it in C means every design choice is visible and intentional. There is no automatic differentiation, no tensor abstraction, no GPU kernel. The matrix multiplies, the gradient accumulators, the Adam moment buffers, the reparameterisation trick — all written explicitly, readable in a single afternoon.

The secondary goal was to write that code at a standard that holds up under engineering review: strict module boundaries, a single-slab heap memory model, per-instance RNG with no shared mutable state, a numerical gradient check in the test suite, and endian-safe binary checkpoints.


Quick Start

# 1. Download MNIST (one-time)
./scripts/download_mnist.sh

# 2. Train on digits 0 & 1
make
./exe/vae_model

# 3. Train on all 10 digits
make full
./exe/vae_model --full-mnist

# 4. Convert generated PGM images to PNG
./scripts/convert_to_png.sh

# 5. Run the test suite
make test

Training is fully resumable — checkpoints are saved to models/vae_vN.bin and reloaded automatically on restart.


Models

v1 v2
Digits 0 & 1 0 – 9
Parameters ~385K ~406K
Architecture 256→128→z32 256→128→z64
Training time ~30 min ~90 min
Speed (serial) ~1,500 img/s ~1,400 img/s
Speed (OpenMP) ~4,100 img/s ~4,100 img/s

Both models use the same hidden layer widths. v2 only doubles the latent dimension (32→64) to accommodate 5× more classes — no extra hidden capacity is needed.


Build Targets

Target Description
make v1 — digits 0 & 1
make full v2 — all 10 digits
make omp-mini v1 with OpenMP (requires brew install libomp)
make omp-full v2 with OpenMP
make omp Both OpenMP variants
make debug Debug build (-g -O0)
make asan AddressSanitizer + UBSan
make test Run all tests
make tsan ThreadSanitizer (validate OpenMP)
make clean Remove all build artifacts

Architecture

This is a Conditional VAE. The digit label is one-hot encoded and concatenated into both the encoder input and decoder input, so the model can generate a specific digit on demand.

 image (784) ─┐
              ├─→ Linear(ELU) → Linear(ELU) → μ, log σ²  [latent]
 label (nc)  ─┘                                    │
                                     z = μ + σ·ε  (ε ~ N(0,I))
                                                   │
 label (nc)  ─┐                                    │
              ├─→ Linear(ELU) → Linear(ELU) → Linear(Sigmoid) → x̂ [784]
 z (latent)  ─┘

Loss (ELBO):

L = BCE(x, x̂) / IMAGE_SIZE  +  β · KL(q(z|x) ∥ N(0,I)) / latent

β is annealed from near-zero up to 0.2 during training, giving reconstruction time to converge before the latent space is regularised. Getting β right is critical — values too small cause posterior collapse and broken generation.

All 10 digits (v2): All digits


Testing

make test
# ALL TESTS PASSED

The test suite covers: RNG determinism and distribution correctness, Adam convergence and gradient ownership contracts, forward pass determinism, loss finiteness, checkpoint roundtrip, backward pass correctness via numerical gradient check, and integration tests verifying loss decreases and KL annealing schedule correctness.


How to Use Saved Model

Once a model has been trained and saved (e.g., to models/vae_v1.bin), you can skip the training phase entirely and generate digits directly from the loaded checkpoint using the --generate flag:

# Generate digits from the v1 (0 & 1) model
./exe/vae_model --generate

# Generate digits from the v2 (all 10 digits) model
./exe/vae_model --full-mnist --generate

The generated .pgm images will be saved to your results_main/ directory.

References


Contributing

If you have recommendations or have spotted bugs, please raise a pull request. See CONTRIBUTING.md for details.


About

A Conditional Variational Autoencoder (CVAE) implementation in C (with training parallelized using OpenMP), trained on MNIST, with zero external dependencies beyond libc and libm.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages