Skip to content

CUDA GPU Workloads in Rust with cuda-oxide

Published:

Description

This project explores CUDA programming in Rust using NVIDIA’s cuda-oxide. It shows how core GPU programming concepts - thread indexing, kernel launches, atomics, streams, asynchronous transfers, matrix multiplication, activation functions, softmax, and profiling - can be implemented from Rust while still fitting into a normal Cargo-based project structure.

The repository is organized as standalone examples exposed through root-level Makefile targets, with additional Triton and PyTorch scripts for framework-level comparisons.

Source code is available on GitHub.

Table of contents

Open Table of contents

Environment

The project targets a Linux machine with an Ampere-or-newer NVIDIA GPU (sm_80+), NVIDIA driver, CUDA Toolkit, compute-sanitizer, LLVM/Clang, Rust nightly, and cargo-oxide.

The Makefile pins the main tooling inputs:

On a Debian/Ubuntu GPU host, the setup target installs the required host tools, Rust toolchain, and cargo-oxide, then runs the environment validation:

make setup

For an already configured GPU host:

make doctor

make doctor checks that nvidia-smi, nvcc, compute-sanitizer, cargo, and cargo-oxide are available, then runs cargo oxide doctor from the first Rust CUDA project.

Implemented GPU workloads

The Rust side contains 15 standalone CUDA examples:

WorkloadWhat was implemented and checked
vector-addAdds two ten-million-element vectors on the GPU and checks every result against CPU addition.
indexingLaunches a three-dimensional CUDA grid, flattens thread IDs, and verifies all flattened IDs are unique and correct.
multidimensional-vector-addRuns equivalent 1D and 3D launch geometries over 100 x 100 x 1000 elements and verifies both outputs against the same CPU reference.
atomicsUses DeviceAtomicU32::fetch_add across 1_000_000 GPU threads and verifies that every returned value from 0..1_000_000 appears exactly once.
streamsUploads independent inputs on separate streams, adds an explicit dependency, and verifies ordered vector addition.
advanced-streamsUses pinned host memory, events, stream ordering, a host callback, and result download validation for (input * 2) + 1.
naive-matmulComputes one matrix output cell per GPU thread and checks every cell against CPU matrix multiplication.
tiled-matmulLoads matrix tiles into shared memory, synchronizes with barriers, and checks every output cell against the CPU result.
profiling-matmulRepeats verified matrix multiplication kernels inside a CUDA-event timing window while keeping transfers outside the measured interval.
profilingBuilds a profiler-friendly repeated matmul workload with warmup, CUDA event timing, and device-side profiling markers.
unrollingCompares loop-based and explicitly expanded GPU kernels over ten million elements and verifies both against a CPU reference.
convolutionImplements same-padded 2D convolution over NCHW tensors and checks the flattened output against expected values.
tanhComputes bounded tanh on the GPU using device arithmetic and checks every output against host f32::tanh() within tolerance.
softmaxImplements numerically stable row-wise softmax and verifies every row against a stable CPU softmax reference.
polynomial-activationComputes y = x * x + x + 1 and checks exact f32 equality for deterministic binary-fraction inputs.

Verification and testing

Local checks

Local checks do not require a GPU:

make check

This target runs formatting and metadata checks for every Rust example:

cargo fmt --all -- --check
cargo metadata --locked --format-version 1 --no-deps

It also byte-compiles the Python scripts and runs the dependency-free stable softmax reference:

python3 -m py_compile python/references/softmax_reference.py python/triton/vector_add.py python/triton/softmax.py python/pytorch/tanh.py
python3 python/references/softmax_reference.py

The Python softmax reference validates a known input and checks that every deterministic output row is normalized.

GPU run targets

Each Rust workload can be built and run from the repository root:

make list
make run-vector-add
make run-tiled-matmul
make run-softmax

All Rust GPU workloads can be executed in order:

make run-all

For numerical examples, a successful run means deterministic input was used, the GPU result was compared with the expected CPU result, and the binary reported PASSED.

GPU sanitizer targets

Every Rust workload has a matching test target:

make test-vector-add
make test-softmax
make test-all

The common test path first runs the workload, then checks the produced release binary with NVIDIA Compute Sanitizer:

compute-sanitizer --tool memcheck ./target/release/<binary>

Additional sanitizer checks are wired into selected examples:

Python reference and framework comparisons

The repository also includes Python comparisons:

make run-python-reference
make run-python-gpu

The CPU-only reference computes stable softmax with the Python standard library. The GPU-backed Python examples require a CUDA-enabled Python environment:

Benchmark targets are available, but they run only after correctness checks pass:

make benchmark-python-triton
make benchmark-python-pytorch

Remote development workflow

The project supports editing locally while running CUDA workloads on a remote Linux GPU host. After exporting the remote connection variables, the root Makefile syncs the repository while excluding generated build output and local editor files:

export REMOTE_HOST="<GPU_HOST>"
export REMOTE_USER="<USER>"
export REMOTE_DIR="~/cuda-course-rust"
# export REMOTE_PORT="<SSH_PORT>"
# export SSH_KEY="$HOME/.ssh/id_ed25519"

make sync

After syncing, the same run and test targets can be executed on the GPU host.

What was tested

The project tests correctness at several layers:

That gives the repository two useful properties: the examples can be run one by one while developing a specific kernel, and the full suite can be exercised with make run-all or make test-all on a configured NVIDIA GPU machine.