Practical guide

Train and run small neural networks

The neural examples implement complete forward passes and parameter gradients with ordinary shdr buffers and compute shaders. Native C++/GLSL pipelines run training and inference on the CPU or GPU. These fixed architectures demonstrate how compute stages and resources form a model. A deeper LeNet-style classifier deploys prepared weights through native inference, with optional offline training in PyTorch.

# Run this example on the CPU.
./build/example_neural
# Run this example on the CPU.
./build/example_cnn
# Run this example on the CPU.
./build/example_transformer
# Run this example on the requested headless GPU profile.
./build/example_neural --gpu es31
# Run this example on the requested headless GPU profile.
./build/example_cnn --gpu es31
# Run this example on the requested headless GPU profile.
./build/example_transformer --gpu es31

Each executable initializes repeatable inputs and parameters, checks gradients, trains the model and reports loss and classification accuracy.

Choose a model

Example Architecture Trainable parameters Seeded task
ANN Two features → eight ReLU neurons → sigmoid output 33 Noisy XOR classification
CNN 8×8 grayscale → two valid 3×3 filters → ReLU → global mean → sigmoid 23 Horizontal versus vertical bars
Transformer Four tokens of width four → attention → normalization → feed-forward residual → two-class head 134 Noisy two-class sequences

Every parameter participates in gradient computation, including hidden layers, convolution coefficients, attention matrices, positional features and classifier heads. The reported loss and classification counts describe the seeded training tasks. The public-data evaluator also fits real MNIST zero/one mini-batches with these models. The deeper classifier has a separate held-out ten-class digit evaluation.

Train an ANN or CNN

neural_support.hpp provides shdr_examples::binary_network. Its first constructor argument chooses the CNN. Its second is the batch size. The constructor supplies seeded data and initial parameters, so this is a runnable CPU training loop:

#include "examples/neural_support.hpp" // Use the small fully trainable binary-network wrapper.
#include <iostream> // Print the evaluated mean loss.
int main() { // Train and evaluate the same model through compute pipelines.
 shdr_examples::binary_network model(false, 32); // Choose the two-feature ANN with a batch of 32 samples.
 for (unsigned step = 0; step < 160; ++step) // Perform the demonstrated number of training steps.
 model.training_pipeline().run(); // Compute analytic gradients and update every parameter on the CPU.
 model.evaluation_pipeline().run(); // Run inference and calculate mean binary cross entropy.
 const shdr::buffer<float>& loss = model.loss; // Read loss through a const host view.
 std::cout << "Mean loss: " << loss.data()[0] << '\n'; // Print the completed mean loss.
} // Release the model resources after evaluation.

Attach its shader sources to the executable with the build helper:

shdr_target_shaders(app PRIVATE SOURCES # Compile and embed each ANN training and evaluation stage.
    examples/shaders/ann_infer.cpp # Compute the network's forward prediction.
    examples/shaders/ann_gradient.cpp # Compute all analytic per-sample parameter gradients.
    examples/shaders/nn_sgd.cpp # Average batch gradients and apply SGD updates.
    examples/shaders/nn_binary_loss.cpp) # Reduce prediction errors into a mean loss.

For a CNN, construct binary_network(true, 32), substitute cnn_infer.cpp and cnn_gradient.cpp, and keep the shared SGD and loss modules. The bundled CNN example trains for 180 updates.

For headless GPU execution, link shdr::egl and use a device:

shdr::headless_device gpu; // Create a default ES 3.1 headless device.
model.training_pipeline().prepare(gpu); // Prepare programs and resources before the training loop.
for (unsigned step = 0; step < 160; ++step) // Perform the same number of GPU training steps.
    model.training_pipeline().run(gpu); // Compute gradients and update parameters on the GPU.
model.evaluation_pipeline().run(gpu); // Evaluate predictions and loss on the GPU.

The training pipeline computes per-sample gradients, then updates parameters using their batch mean. Prediction is a separate inference stage, and evaluation adds mean binary cross-entropy. gradient_pipeline() writes derivatives and preserves parameter values. reset_parameters() restores the deterministic initial weights.

Public buffer Contents
inputs Sample-major features: two floats per ANN sample or 64 per CNN image
targets One binary target, zero or one, per sample
parameters All trainable weights and biases
gradients batch_count * parameter_count sample-major derivatives
predictions One sigmoid probability per sample
loss One batch-mean binary cross-entropy value after evaluation

Supply custom data by writing the allocated input and target buffers. Use parameter_count() and batch_count() for dimensions. The default SGD learning rates are 0.4 for the ANN and 1.5 for the CNN. These settings belong to the small seeded fixtures. Tune training settings when building another model or dataset.

Train the transformer

transformer_support.hpp provides a four-token classifier with a configurable batch size and learning rate:

#include "examples/transformer_support.hpp" // Use the four-token transformer demonstration wrapper.

shdr_examples::transformer_model model(8, 0.15f); // Allocate eight samples and use a learning rate of 0.15.
for (unsigned step = 0; step < 32; ++step) // Perform the demonstrated transformer training steps.
    model.training_pipeline().run(); // Compute analytic gradients and update all model parameters.
model.prediction_pipeline().run(); // Run a separate forward prediction dispatch.
const float mean_loss = model.loss(); // Calculate the mean loss from completed predictions.
const unsigned correct = model.correct(); // Count samples whose predicted class matches their label.

Attach transformer_infer.cpp, transformer_gradient.cpp and nn_sgd.cpp to the executable. Both model pipelines accept run(gpu) for GPU execution. reset_inputs(seed) recreates the balanced sequence fixture, while reset_parameters(seed) restores initial weights. Replace inputs with sample-major, token-major floating features and labels with class IDs zero or one to supply a compatible batch.

The forward pass adds learned positional features, computes one scaled Q/K/V attention head, projects its output and adds a residual. RMS normalization has learned scales. A width-four ReLU feed-forward block adds another residual. Mean token pooling feeds the two-class classifier. Softmax and cross-entropy use shifted logits for numerical stability.

Parameter indices Learned family
0–15 Four positional feature vectors
16–31 / 32–47 / 48–63 Query / key / value matrices
64–79 Attention output projection
80–83 RMS normalization scales
84–99 / 100–103 First feed-forward weights / biases
104–119 / 120–123 Second feed-forward weights / biases
124–131 / 132–133 Classifier weights / biases

inputs holds sixteen floats per sequence. parameters holds 134 floats, and gradients holds batch_count * 134 floats. Each predictions element is vec4(probability_class_0, probability_class_1, cross_entropy, predicted_class). The predicted class is zero or one represented as a float. loss() and correct() read the completed prediction buffer and can download GPU results.

The transformer gradient uses analytic forward-mode differentiation: vec2 holds a value and one parameter derivative. Each (sample, parameter) invocation evaluates a forward pass, and the shared SGD stage reduces the batch. This independent-invocation design trains every parameter and repeats forward work for each derivative. Large models benefit from the efficiency of reverse-mode differentiation.

Understand the update pipeline

Training kernels write gradients[sample * parameter_count + parameter], so each invocation owns a distinct output. The nn_sgd_kernel gives each parameter its own invocation and applies:

parameter -= learning_rate * (mean_sample_gradient + weight_decay * parameter)

Its named ports are gradients and parameters. Its uniforms are batch_count, parameter_count, learning_rate and weight_decay. The examples use zero weight decay. The gradient stage finishes before the update dispatch reads its output. Parameter writes then become visible to subsequent inference or training stages. At most four storage-buffer ports are used per model stage, within the guaranteed ES 3.1 compute baseline.

Inference reads trained parameters and writes predictions. Attach the inference module, create its kernel with make_kernel, bind input, parameter and output buffers, and set batch_count. The transformer also binds labels for its loss diagnostic. Predicted probabilities depend on inputs and parameters. Applications use the embedded shader programs.

Run a LeNet-style digit classifier

shdr_examples::lenet_classifier in lenet_support.hpp runs a modern LeNet-5-style network with 61,706 parameters. It uses full second-layer connectivity, ReLU, fixed average pooling and linear final logits. These choices differ from the original 1998 LeNet-5 architecture.

Stage Per-image output
5×5 convolution, one input channel, six output channels, ReLU 6×28×28, with a two-pixel input zero border
2×2 average pooling 6×14×14
5×5 valid convolution, six input channels, sixteen output channels, ReLU 16×10×10
2×2 average pooling 16×5×5
Dense layer and ReLU 120 features from 400 flattened values
Dense layer and ReLU 84 features
Dense classifier Ten logits

The seven stages reuse three native modules: lenet_conv.cpp, lenet_pool.cpp and lenet_dense.cpp. Attach them to the executable with shdr_target_shaders. Input and intermediate buffers stay on the selected backend until their results are needed on the host.

#include "examples/lenet_support.hpp" // Use the seven-stage native classifier.
#include "examples/real_data_support.hpp" // Read the prepared plain binary fixtures.
auto weights = shdr_examples::real_data::floats("lenet_weights.bin"); // Load the prepared flat float32 parameters.
auto images = shdr_examples::real_data::bytes("mnist_test.bin"); // Read held-out digit image bytes.
shdr_examples::lenet_classifier model(1); // Allocate activations for one digit image.
model.load_parameters(weights); // Check the parameter count and finite values.
auto* input = model.inputs.data(); // Access the host input before dispatching inference.
for (unsigned i = 0; i < 784; ++i) { // Copy the first 28-by-28 held-out image.
 input[i] = images[i] / 255.f; // Match the zero-to-one training input scale.
} // The convolution stage supplies the zero border internally.
model.inference_pipeline().run(); // Or run(gpu) for the same complete GPU network.
const shdr::buffer<float>& scores = model.logits; // Read scores through a const host view.
const float* logits = scores.data(); // Obtain ten completed unnormalized class scores.

Choose the class with the largest logit. The helper accepts batches from one to 512 images. inputs contains batch_count * 784 floats and logits contains batch_count * 10. Parameters use convolution order [out,in,ky,kx] and dense order [out,in], with each layer's biases immediately after its weights.

Layer Weight offset Bias offset
First convolution 0 150
Second convolution 156 2556
Dense 120 2572 50572
Dense 84 50692 60772
Dense 10 60856 61696

The included model was prepared with three complete epochs of the 60,000-image MNIST training split, Adam at learning rate 0.001, batch size 128 and seed 12345. Independent PyTorch evaluation of all 10,000 held-out images recorded 97.78% accuracy. These are the model-preparation results in training provenance. The real-data executable separately checks native CPU/GPU logits against the framework scores and reports accuracy against the original test labels. Full native runs reproduced 97.78% on every recorded CPU/GPU path. The data results contain the measured accuracy, numerical errors and evaluation times.

Preparation is optional. To retrain and export plain weights, install PyTorch and NumPy in a preparation environment, then use the checked source cache:

# Train on the official training split and export weights and independent test logits.
python3 scripts/train_lenet.py --cache build/datasets/upstream

The deployed C++ program reads lenet_weights.bin and runs the seven-stage classifier through the single-header runtime. The three smaller models demonstrate native analytic training. See the data guide for preprocessing, source terms, preparation packages and the full held-out evaluation.

Validation and limits

The ANN and CNN compare every analytic parameter gradient with central differences from an independent double-precision prediction oracle. GPU runs compare gradients, trained weights and probabilities with CPU results. The transformer checks predictions and all 134 analytic derivatives against its independent double-precision oracle. GPU runs compare parameters, gradients and predictions with a separately trained CPU model, and check parameter-family changes, loss reduction and classification. Finite differences validate the implementations. Training uses the analytic gradients.

The ANN uses eight fixed ReLU neurons. The CNN uses stride-one valid cross-correlation and two 6×6 activation maps. The transformer uses one attention head over all four fixed tokens. These examples train fixed float32 architectures with SGD. Model import, serialization, mixed precision, large tensor operations, distributed training and adaptive optimizers require additional application-specific implementations. Compare CPU and GPU float results with tolerances suitable for the algorithm, particularly near ReLU boundaries.

Keep inputs and parameters stable during a dispatch. Serialize training jobs that share parameter storage. Concurrent inference can share immutable parameters and inputs while using distinct writable outputs. Read GPU losses and predictions at deliberate synchronization points. See asynchronous execution for background work and finite resource rings, and portability for source and layout rules.