Orthon is a deep learning runtime built from the storage layer up: a native
C++17 tensor engine with strides and storage-sharing views, a dynamic autograd
graph, handwritten CPU and CUDA kernels, pybind11 bindings, an nn.Module
training stack with optimizers and mixed precision, a pickle-free checkpoint
format, and single-node distributed fundamentals.
It is a systems project. The point is that every layer — from x * y in
Python down to the size-class allocator behind it — is legible, explained, and
pinned by tests, with PyTorch used throughout the suite as the numerical
reference. It is not a production replacement for PyTorch and does not claim
to be.
Why it is interesting
A few of the design decisions worth reading for, each covered in depth in
docs/:
- Selective saved-tensor version checking. Autograd nodes watch the
storage-level mutation generation of exactly the tensors their backward
formula reads — so mutating an operand of
x * ybeforebackward()fails loudly with the node and role named, while mutating the operand ofx * 3stays legal, because d(x·3)/dx never readsx. Wrong-gradient bugs become errors; harmless mutation stays allowed. (docs/autograd.md) - Backward that structurally cannot corrupt itself. Node backward formulas are written in public differentiable ops, so the whole traversal runs under one RAII no-grad guard — gradients cannot come back carrying a second graph, by construction rather than by per-node convention.
- Real views with correct gradients.
transpose,sliceand indexing share storage via strides and offsets; their gradients route back through the view transformation in logical coordinates. - Contiguous-only CUDA kernels, normalized once. No kernel is taught about strides. Public ops linearize strided operands at one auditable boundary, while the autograd node stays attached to the original tensor — graph shape, mutation watches and view routing all describe the caller's program. (docs/cuda_runtime.md)
- Exact-device CUDA discipline. Every allocation, copy and launch is
guarded to its operand's own device (RAII
DeviceGuard), never the ambient CUDA context; cross-GPU copies name both ordinals viacudaMemcpyPeer. The single-GPU/multi-GPU validation boundary is stated honestly. - Rank-0 scalar arithmetic.
x * 2builds an O(1) rank-0 constant on the tensor's exact device and dtype — no data-shaped temporary, and no spurious mutation watch. - A checkpoint format that cannot execute code. ZIP + JSON manifest + raw tensor bytes, SHA-256 per tensor, deterministic archives, atomic saves, validate-everything-before-mutating loads; every dtype round-trips bit-for-bit, NaN payloads included. (docs/checkpoint_format.md)
Core capabilities
- Tensors — shape, strides, dtype, device, storage offset and contiguity,
with storage-sharing views (
reshape,transpose,slice, integer index). Nine dtypes:float32,float64,float16,bfloat16,int32,int64,uint8,int8,bool. - Autograd — dynamic graph, reverse topological traversal, gradient
accumulation, retained graphs, broadcast-gradient reduction, differentiable
.to()/cast(), and stale-mutation detection. - CPU and CUDA — one public API over both. Handwritten CUDA kernels for matmul (2-D and batched 3-D), Conv2d, MaxPool2d, unary/binary ops, reductions, max/argmax, softmax, cross entropy, NLL loss, embedding and dropout, plus a caching memory pool with a reusable scratch workspace. cuBLAS Tensor Core GEMM backs FP16/BF16 matmul where shape and device fit.
- Training stack —
Linear,Conv2d,MaxPool2d,Dropout,LayerNorm,Embedding, BatchNorm, attention modules and a Transformer encoder block; SGD and Adam; one seeded framework RNG behindmanual_seed. - Mixed precision — FP32 master weights (
MixedPrecisionOptimizer) and a dynamicGradScalerwith finite checks, skipped steps, backoff and growth; under DDP, ranks synchronize non-finite-gradient decisions. - Checkpointing — one versioned format (
ml-runtime-checkpointv1) for model state, optimizer state and metadata; no pickle anywhere. - Distributed fundamentals — a local multiprocessing backend and a single-node NCCL backend, DDP gradient hooks, buckets and async work handles.
Architecture
flowchart TD
A["Python: nn / optim / amp / distributed / checkpoint"] --> B["pybind11 extension (orthon._C)"]
B --> C["C++17 tensor engine (namespace orthon)<br/>storage · strides · views · broadcasting"]
C --> D["dynamic autograd graph"]
C --> E["CPU / CUDA dispatch"]
E --> F["CPU kernels"]
E --> G["CUDA kernels · cuBLAS · optional NCCL"]
F --> H["memory pool · device runtime"]
G --> H
A call travels down and back: Python reaches a bound op, the op dispatches on
its operands' device and dtype, an autograd node is attached when an input
requires grad, backward() walks the graph in reverse topological order, and
the optimizer applies the update — optionally through AMP FP32 master weights.
docs/architecture.md walks the full stack.
Quick start
pip install -e . # CUDA auto-detected; absent means CPU-only
import orthon as ortho
from orthon.nn import Linear, ReLU, Sequential
from orthon.optim import SGD
model = Sequential(Linear(4, 16), ReLU(), Linear(16, 2))
x = ortho.ones([8, 4])
target = ortho.ones([8, 2])
opt = SGD(model.parameters(), lr=0.01)
for _ in range(10):
opt.zero_grad()
error = model(x) - target
loss = ortho.mean(error * error)
loss.backward()
opt.step()
print(loss.item())
Autograd in a few lines
import orthon as ortho
x = ortho.ones([2, 3]).requires_grad_(True)
y = x * x + 3.0 # scalar: rank-0 constant, no temporary
z = ortho.sum(y.transpose(0, 1)) # storage-sharing view mid-graph
z.backward()
print(x.grad.tolist()) # [2.0, 2.0, 2.0, 2.0, 2.0, 2.0]
And the mutation safety that distinguishes the engine:
a = ortho.ones([3]).requires_grad_(True)
b = a * a # MulNode records a's storage version
a.add_(a, alpha=1.0) # in-place mutation bumps it
ortho.sum(b).backward() # raises: "tensor saved by MulNode as 'lhs' was
# modified in-place after it was saved"
The check is selective — b = a * 3.0 followed by the same mutation and
backward succeeds, because that derivative never reads a. Details in
docs/autograd.md.
Mixed-precision training on CUDA
import orthon as ortho
from orthon.amp import GradScaler, MixedPrecisionOptimizer
from orthon.nn import Linear, ReLU, Sequential
from orthon.optim import Adam
model = Sequential(
Linear(512, 1024),
ReLU(),
Linear(1024, 10),
).to("cuda").half()
opt = MixedPrecisionOptimizer(model, Adam, lr=1e-3)
scaler = GradScaler(init_scale=128.0)
x = ortho.ones([256, 512]).to("cuda").cast("float16")
opt.zero_grad()
out = model(x)
loss = ortho.sum(out * out)
scaler.scale(loss).backward()
scaler.step(opt)
scaler.update()
Checkpointing
ortho.save_checkpoint(
"run.ckpt", model=model, optimizer=opt, metadata={"step": 100},
)
metadata = ortho.load_checkpoint(
"run.ckpt", model=model, optimizer=opt, map_location="cuda",
)
Loading validates the whole archive — manifest, byte counts, SHA-256 per tensor — before touching the destination, so a malformed file cannot leave a model half-updated, and nothing in the file can execute code.
Building
CPU:
pip install -e . # CUDA auto-detected; absent means CPU-only
pip install -e ".[test]" # adds pytest and PyTorch for the test suite
pytest -q
PyTorch is a numerical reference for the parity tests, not a runtime dependency; without it the suite still runs and the comparison tests skip.
CUDA (WITH_CUDA=AUTO by default; force or forbid it):
SKBUILD_CMAKE_DEFINE="WITH_CUDA=ON" pip install -e .
SKBUILD_CMAKE_DEFINE="WITH_CUDA=OFF;WITH_NCCL=OFF" pip install -e .
Plain CMake (native library, tests, extension):
cmake --preset cpu-release && cmake --build --preset cpu-release
cmake --preset cuda-release # CUDA kernels, no NCCL
cmake --preset cuda-nccl-release # CUDA + NCCL collectives
orthon.cuda_enabled() reports what was compiled in, so a CPU-only build says
so rather than failing at the first kernel launch.
Testing and validation
python -m pytest -q # Python suite
cmake --preset cpp-cpu-release # native C++ suite
cmake --build --preset cpp-cpu-release
ctest --preset cpp-cpu-release
Multi-GPU validation, run on a Linux machine with two NVIDIA A100-SXM4-80GB GPUs (CUDA 12.8, GCC 13.3, NCCL 2.27.3):
| Suite | Result |
|---|---|
| Python, CUDA+NCCL build (2x A100) | 3426 passed, 20 skipped, 0 failed |
| Native C++, Linux/GCC CUDA (2x A100) | 186 discovered, 186 passed, 0 skipped |
Those two rows are a record of that run and are not restated for the current tree, which has since gained six autograd regression tests.
The remaining 20 Python skips are hardware/configuration gates — most need
four or eight GPUs, which that machine does not have. The three native
DeviceGuard tests that need a second GPU, which skip on single-GPU
machines, executed there and passed. That run covers the tree as it stood
immediately before one later, device-agnostic autograd correction; the exact
boundary is in docs/ddp_cloud_validation.md.
Current tree, on the single-GPU development machine:
| Suite | Result |
|---|---|
| Python, CPU build | 2186 passed, 889 skipped |
| Python, CUDA build (RTX 4070 Laptop, 1 GPU) | 3397 passed, 60 skipped |
| Native C++, Windows/MSVC CPU | 182 / 182 |
| Native C++, Windows/MSVC CUDA (1 GPU) | 191 discovered, 188 passed, 3 skipped |
| Native C++, Linux/GCC CPU | 182 / 182 |
Zero failures in every configuration; every case a machine cannot honestly
run skips explicitly rather than pretending. CI covers CPU only,
deliberately: GitHub-hosted runners have no GPU, and a CUDA job on one would
report green while proving nothing. docs/testing.md has
the full picture.
Selected Orthon vs. PyTorch results
Orthon is not intended as a replacement for PyTorch. It was built to understand and implement the systems underneath modern ML frameworks. But once the kernels exist, the honest question is how they measure up — so they were measured against PyTorch eager on the same GPU, with inputs pre-placed on the device, alternating sample rounds, and numerical equivalence proved before every timing.
On the RTX 4070 Laptop GPU used for Orthon's benchmarks, two specialized paths
outperform PyTorch eager reliably enough to quote. Each is an Orthon win in all
four independent runs, is never flagged noisy, and matches PyTorch's output to
within 1e-07 relative (the forward value matches exactly).
Cross entropy, forward — 2048 × 4096
Orthon 0.175 ms
PyTorch 0.536 ms
Speedup 3.06x (2.45x - 3.06x across 4 runs)
Cross entropy, forward + backward — 4096 × 4096
Orthon 3.143 ms
PyTorch 4.523 ms
Speedup 1.44x (1.36x - 1.44x across 4 runs)
One fused kernel reads the logits once instead of materializing a full
log-softmax and gathering. The advantage is bounded and the boundary is
published: it depends on a large class count, and reverses at 8192 × 512,
where PyTorch is 4.1x faster.
PyTorch wins 16 of the 36 cases, Orthon 9, with the rest ties or too close to
call — FP32 matmul goes to PyTorch by up to 6.4x and Conv2d by up to 11.8x.
A third result, Fused Linear + ReLU, was previously quoted here at 1.68x; it
was measured against an eager composition slower than PyTorch's own
F.linear, and against the correct baseline that shape is a PyTorch win, so
the claim has been withdrawn rather than replaced. The complete benchmark
matrix, the methodology, the numerical-equivalence checks and every losing case
are published in
profiles/PYTORCH_COMPARISON.md.
python benchmarks/compare_pytorch.py # the whole matrix
python benchmarks/compare_pytorch.py --highlight cross_entropy --shape 2048 4096
Project layout
include/orthon/ Public C++ headers (namespace orthon)
src/ Tensor engine, autograd, CPU/CUDA kernels, memory/device code
bindings/ pybind11 extension (orthon._C)
python/orthon/ Python package: nn, optim, amp, distributed, checkpointing
tests/python/ pytest correctness, reference and architecture tests
tests/cpp/ C++ unit tests (GoogleTest, run via CTest)
tests/smoke/ Packaging smoke test for an installed wheel
examples/ Training workloads and profiling drivers
benchmarks/ Focused benchmark scripts and the PyTorch comparison harness
docs/ Architecture, format, coverage and workload documentation
profiles/ Profiling notes and benchmark results
.github/workflows/ CPU continuous integration
Scope and known limitations
- Multi-GPU validation covers two GPUs. Orthon was validated on real
2x A100 hardware — exact
cuda:Nplacement, cross-GPU transfer and autograd, NCCL collectives, and multi-step DDP in every synchronization mode all ran there. That run predates one later autograd correction, which is device-agnostic and left every device path byte-identical. The>=4/8-GPU test gates were not rerun; an earlier project state has a recorded 2/4/8-GPU cloud run (details). On single-GPU machines every>=2-device test still skips explicitly. - CUDA in-place ops require contiguous destinations, and partial CUDA
__setitem__is unsupported — both raise rather than silently degrading. - CUDA kernels are contiguous-only by design; views are normalized at the public op, never inside kernels.
softmaxis float32-only; CUDA compute covers float32/float16/bfloat16, with float64 compute on CPU.- Checkpoints are single-file and local — not sharded or distributed.
- Benchmark numbers are narrow, hardware-specific microbenchmarks, taken on one RTX 4070 Laptop GPU against PyTorch eager. Orthon loses more of the matrix than it wins; every losing case is published in PYTORCH_COMPARISON.md. No performance timing anywhere in this repository comes from the 2x A100 machine, which produced multi-GPU correctness evidence only.
The full lists live with the layer that owns each limit: cuda_runtime.md, training_stack.md, ddp_cloud_validation.md.
Documentation
| Document | What it covers |
|---|---|
docs/architecture.md |
The complete stack, Python API to allocator |
docs/autograd.md |
Graph construction, backward, views, mutation safety |
docs/cuda_runtime.md |
Device model, kernels, dtypes, validation boundary |
docs/training_stack.md |
nn.Module, optimizers, AMP, RNG |
docs/checkpoint_format.md |
The on-disk format and its safety properties |
docs/testing.md |
Test layers, per-configuration counts, CI, CUDA limits |
docs/cuda_coverage.md |
Per-operation CPU/CUDA/autograd support matrix |
docs/training_workloads.md |
Example workloads and how to run them |
docs/ddp_cloud_validation.md |
Distributed: supported vs validated |
docs/nsight_profiling.md |
CUDA profiling workflow |
profiles/PYTORCH_COMPARISON.md |
Orthon vs. PyTorch eager: full matrix, methodology, losses |
profiles/RESULTS.md |
Recorded benchmark results |
Log in or sign up for Devpost to join the conversation.