← Back
Home › Systems › AI and ML infrastructure

tensor-runtime

Loads a trained model from a file and runs it on whatever hardware is available behind one API, optimizing the graph of tensor operators first.

ONNX Runtime

Open the tensor-runtime pagetensor-runtime Dataflow analysiscompilers: Compute a fact per program point (live variables, reaching definitions, available expressions) by iterating transfer functions over the CFG to a fixed point.›Shape inferencecompilers: Propagating tensor shapes and dtypes through an operator graph, with symbolic dimensionsWork-stealing dequeconcurrency: Per-worker deque: the owner pushes and pops at the bottom without contention, idle workers steal from the top; the core of Rayon, Tokio and Go.›Topological sortgraphs: Order the vertices of a DAG so every edge points forward; the core of build systems, schedulers and dependency resolution, with cycle detection for free.›mmap and the page cachehardware-os: How read() and mmap reach file data through the kernel page cache, what readahead and madvise do, and why databases use their own buffer pool instead.›SIMD programminghardware-os: Process 4-16 lanes per instruction with AVX2, AVX-512 or NEON via Rust std::arch, with runtime dispatch and a portable fallback.›Cuda graphsml-training: Capturing a sequence of GPU kernel launches once and replaying it to remove CPU launch overheadGPU compute shadersml-training: The vendor-neutral GPU launch model of Metal, Vulkan and WebGPU: a grid of workgroups, shared memory, barriers and subgroups, driven by recorded command buffers.›Kernel fusionml-training: Merge chains of memory-bound GPU ops into one kernel, so intermediates stay in registers instead of round-tripping through HBM.›Model file formatsml-training: Weight file layouts (safetensors, GGUF, ONNX): headers, alignment for mmap, tensor metadata, quantized blocksOperator graph partitioningml-training: Splitting a model graph across CPU/GPU/NPU backends by supported ops and transfer costTensor layout transformsml-training: Choosing and converting tensor layouts (NCHW/NHWC, blocked, tiled) for kernel efficiencyTensor memory planningml-training: Static buffer reuse from tensor lifetimes so a graph runs in a fixed arena with minimal peak memoryTiled matrix multiplicationml-training: Block GEMM into tiles held in shared memory and registers, so each byte loaded from DRAM feeds many FMAs; the core of fast GEMM.›Weight quantizationml-training: Store weights as 8/4-bit integers with per-group scales; decode is memory-bound, so 4-bit weights give about 3-4x tokens/s and fit 4x larger models.›Protobuf wire formatnetworking: Protocol Buffers wire format: tags, varints, length-delimited fields and schema evolutionArena allocatorsystems: Allocate by bumping a pointer through big chunks and free everything at once: a few instructions per allocation for objects sharing a lifetime.›Control-flow graphcompilers: Split a linear instruction list into basic blocks and connect them by jumps; the graph every optimizer, SSA builder and dataflow pass walks.›Epoch-based reclamationconcurrency: Threads announce a global epoch while touching shared lock-free data; garbage retired in epoch e is freed once the epoch reaches e + 2.›Memory orderingconcurrency: What Relaxed, Acquire, Release and SeqCst atomics actually guarantee, what they cost on x86 and ARM, and which one each pattern needs.›BFS and DFSgraphs: The two basic graph traversals; BFS gives hop distances and levels, DFS gives discovery/finish order for cycles, components and topological order.›Virtual memory and the TLBhardware-os: How page tables and TLBs translate every address, what TLB reach means, and when 2 MiB or 1 GiB huge pages pay off on Linux.›Graph partitioninggraphs: Split a graph into k balanced parts with few edges between them; multilevel coarsen-partition-refine (METIS, KaHIP) for quality, streaming heuristics (LDG, Fennel) for one-pass scale.›Gpu memory coalescing bankinghardware-os: Coalesced global loads, shared memory bank conflicts, L2 and HBM bandwidth on GPUsCUDA and GPU languageslanguages: Programming GPUs: thousands of lockstep threads, bound by memory bandwidth; libraries first, Triton for fused kernels, CUDA C++ for peak speed.›Low precision number formatsnumerics: Bf16, fp16, fp8 (E4M3, E5M2), MX block formats and int4: ranges, rounding, scalingLU and Cholesky factorizationnumerics: Factor A = PLU (any square matrix, partial pivoting) or A = LLᵀ (SPD) once in O(n³), then solve each right-hand side in O(n²).›Sparse matrices (CSR)numerics: Compressed sparse row stores only nonzeros as row pointers, column indices and values; SpMV streams them once, so it is memory-bandwidth bound.›d-ary heapdata-structures: Array heap with d children per node (d = 4 typical): half the binary heap's depth, siblings in one cache line, faster priority queues.›PRNGs (PCG, xoshiro)numerics: Small, fast, statistically strong non-cryptographic generators: PCG32 and xoshiro256**, with seeding, parallel streams and unbiased ranges.›

How it is built

Architecture overview

A portable tensor runtime loads a trained model from a file and executes it on whatever hardware is at hand (server CPU, GPU, phone, browser) behind one API. It is an interpreter for a graph of tensor operators with a compiler's optimizer in front of it. At load time it parses the model, infers shapes, rewrites the graph (constant folding, operator fusion, layout changes), splits the graph among hardware back ends, and plans the memory of every intermediate tensor. At run time an executor walks the nodes in dependency order and calls one kernel per node, or per fused node, on a thread pool or a GPU stream.

The idea that makes it work is a stable operator contract plus replaceable back ends. The model format fixes the meaning of each operator; each back end declares which operators it can run and supplies its own kernels; the runtime assigns each node to the best back end that claims it and falls back to a reference CPU kernel for the rest. A model exported once therefore runs everywhere, and a new chip only needs a back end, not a new model. ONNX Runtime is the widest open example of this design; its back ends are called execution providers.

Parts:

 model.onnx / .ort / .gguf
        │ parse (protobuf or flat binary), weights mmap'd
        ▼
  in-memory graph ──► shape inference ──► basic rewrites (fold constants, fuse, drop no-ops)
        │
        ▼ partition: each node → first back end that claims it (GPU, NPU, … else CPU)
  subgraph A (GPU) │ subgraph B (CPU) │ subgraph C (NPU)
        │             extended / layout rewrites, per back end
        ▼
  memory plan: lifetimes → offsets in one arena per device
        ▼
  executor: topological order ──► kernel per node ──► intra-op thread pool / GPU stream
        ▲                                │
  inputs ─────────────────────────────────┴──► outputs     (session reused across requests)

Everything left of the executor is paid once per session; everything right of it is paid per request. The design question at each seam is how much work can move from the second group to the first.

How the parts fit together

Model loader

The runtime's input is a file, so the file format is the first contract. ONNX stores the model as a protobuf message (onnx.ModelProto) holding the graph, operator-set versions, and the weights as initializers (ONNX IR specification, see Sources). Decoding it is a walk over Protobuf wire format (no page yet) fields: tags, varints and length-delimited blobs, with weight tensors as large byte blobs. Protobuf messages have a 2 GB size limit, so large ONNX models keep their tensors in separate external files referenced from the model (ONNX External Data doc). Other formats pick differently: GGUF (used by llama.cpp) is a flat binary with a metadata header and a tensor table, aligned so the file can be mapped straight into memory (GGUF spec, "mmap compatibility"); ORT's own .ort format is a smaller serialised graph meant for reduced builds on mobile. Model file formats (no page yet) is the block that compares them; the runtime-level choice is whether the file is a graph description (portable, needs an optimizer) or a pre-lowered plan (fast to load, tied to a back end and version).

Weights dominate the size, so the loader should not copy them. With mmap and the page cache, weights become read-only file-backed pages: load time is near zero, the operating system shares one copy among processes, and pages are fetched on first touch. The cost is a slow first request (page faults) and tail latency if the file is evicted under memory pressure; for latency-critical serving the weights are touched once at startup (prefetched or "warmed") so the faults happen before traffic arrives.

Graph analysis

Shape inference (no page yet) runs operator by operator and fills in the shape of every tensor from the input shapes and the operator rules. Static shapes make everything downstream a constant; dynamic dimensions (batch size, sequence length) are carried symbolically or fixed per call. Dataflow analysis finds which nodes depend only on constants (to fold at load time), which tensors are read by several consumers (which blocks fusion), and when each tensor is last read (which the memory planner needs). The result is a graph annotated with shapes, types, constants and lifetimes.

Graph optimizer

ONNX Runtime divides its graph optimizations into levels (graph-optimizations doc, see Sources). Basic rewrites are semantics-preserving and run before partitioning, so they apply to every back end: constant folding, removal of redundant nodes (identity, dropout in inference), and simple fusions such as folding a bias add or batch norm into a convolution. Extended rewrites are complex fusions and run after partitioning, only on the nodes assigned to the back ends that implement them. Layout optimizations (for example NCHWc blocking for CPU convolution) run after partitioning and apply to the CPU back end. The order is the lesson: portable, back-end-independent rewrites first; rewrites that need to know the hardware only after nodes have been assigned.

Kernel fusion is the rewrite with the largest effect: a fused node reads its input once and writes its output once, instead of writing and re-reading every intermediate. Tensor layout transforms (no page yet) change the order of dimensions to what a kernel wants; each change that has to be undone later costs a full transpose, so the optimizer must count it. Optimized graphs can be saved (offline mode) so the next process skips the work; the docs note that the saved graph must be used with the same back ends and hardware it was optimized for, because the rewrites embed hardware-specific nodes.

Partitioner

Operator graph partitioning (no page yet) is what makes the runtime portable. In ONNX Runtime each execution provider implements GetCapability(), returning the nodes or subgraphs it can run (execution-providers doc). The application passes an ordered list of providers, for example CUDA first and CPU last; each node goes to the first provider that claims it, and the default CPU provider is the fallback for everything else. A back end that claims whole subgraphs (TensorRT, OpenVINO) is handed a connected region, which it compiles into one engine; a back end that claims single nodes (the CPU provider) is called node by node.

The contract: the partitioner owns the cut, the back end owns the kernels inside. The price is at every cut edge: a tensor leaving a GPU subgraph to a CPU node is a device-to-host copy. A graph that alternates between two back ends many times can be slower than one that stays on a single back end, even if each back end is faster per node. The practical rule is to prefer fewer, larger partitions and to keep an unsupported operator from splitting a hot region (rewrite it, or add the operator to the back end).

Memory planner

Intermediate tensors are allocated by a plan, not by malloc per node. Tensor memory planning (no page yet) uses the lifetimes from the analysis to assign each tensor an offset in a single buffer, reusing the bytes of tensors that are no longer alive. The buffer comes from an Arena allocator: one large block obtained up front, handed out by moving a pointer, freed in one step. Per request the runtime allocates nothing; peak memory is known after planning and does not drift.

The GPU back end uses the same idea with a pool (the CUDA provider documents gpu_mem_limit and an arena_extend_strategy option, see Sources). Two details matter in production: planning needs known shapes (with dynamic shapes the arena is sized for the largest request seen, or the plan is recomputed), and one session's arena is not shared between concurrent requests unless the session is run with separate buffers per call.

Executor

The executor needs an order in which every node runs after its inputs: a Topological sort of the graph, computed once and stored. ONNX Runtime has two modes: sequential (the default) runs nodes one at a time, and parallel mode runs independent branches of the graph concurrently, controlled by the intra-op and inter-op thread counts (thread-management doc). Parallelism inside an operator (splitting a matmul's rows among cores) is the intra-op pool; parallelism between operators is the inter-op pool. The intra-op default is one thread per physical core.

Both pools consist of many short tasks, which is the case for a Work-stealing deque: each worker owns a local queue and idle workers steal from others, so load balances without a shared lock. The docs also describe thread spinning (waiting threads busy-loop for a bounded time before sleeping), which lowers latency per call and costs CPU time; for a service with many sessions on shared cores, turning spinning off or limiting it is a common setting.

Kernel library

Each back end ships kernels. On CPU, Tiled matrix multiplication is the core of the dense operators: blocks sized to cache, an inner kernel written with SIMD programming (vector registers and fused multiply-add). On GPUs, kernels are written in a vendor language or as GPU compute shaders, which is also how a browser or cross-vendor back end (WebGPU) runs the same operators without CUDA. Weight quantization stores weights as int8 or int4 with scales: the file is smaller, memory traffic per token drops, and the kernel dequantizes on the fly or uses integer instructions. The contract with the planner is that quantized operators have their own dtypes and shapes, so they need a kernel per type, which is where operator coverage gaps usually come from.

GPU launch layer

On a fast GPU with small kernels, launching kernels from the CPU can cost more than running them. CUDA graphs (no page yet) record a sequence of kernel launches once and replay it with one call. ONNX Runtime's CUDA provider exposes this as the enable_cuda_graph option (documented as preview). Its documented constraints define the contract: every node must be assigned to the CUDA provider, control-flow operators are not supported, and shapes and addresses of inputs and outputs must not change between calls, so inputs are bound with I/O binding and copied into the fixed buffers. Several graphs can be captured per session, selected by a gpu_graph_id run option, which is how multiple input shapes are handled. It pairs with the memory plan: fixed offsets make a graph replayable.

Data and control flow

Loading a session (once).

  1. Open the model file; parse it (protobuf decode, or map a flat file); map or load the weights.
  2. Infer shapes and run the basic rewrites; fold constants.
  3. Partition by the provider list; each provider compiles or prepares its subgraphs; run extended and layout rewrites.
  4. Plan memory, create the arenas and thread pools, optionally record GPU graphs, and warm the weights.
  5. Optionally save the optimized graph so the next load skips steps 2 to 4.

One request.

  1. Bind the inputs (host tensors, or tensors already on the device to avoid a copy).
  2. The executor runs the stored order. Each node: look up input pointers from the plan, call the kernel.
  3. Intra-op parallel kernels split work across the pool; GPU kernels are queued on a stream; replay launches a whole recorded graph.
  4. Copy the outputs back if they must leave the device.

Time goes where the model spends it: matmul-heavy nodes dominate, then memory-bound elementwise nodes (which fusion shrinks), then copies at partition edges and kernel launch gaps on GPUs.

Scaling

In the order you would pull the levers:

  1. Per-request latency: optimization level on, offline-optimized model, fused kernels, weights warmed, a GPU graph for small kernels.
  2. Throughput per machine: batch requests (dynamic batching in front of the runtime), tune the intra-op thread count to the cores per session, run several sessions with fewer threads each instead of one with all of them.
  3. Memory: quantize the weights, share weights between sessions through mmap, size arenas to the real maximum shape.
  4. Operator coverage: every unsupported operator is a partition cut; add the kernel, rewrite the model, or accept the fallback on a cold path.
  5. Fleet: the runtime is stateless per request, so scale by adding replicas; the model file is the only artifact to distribute and it should be version-pinned together with the runtime and provider versions.

Failure modes

FailureSymptomCauseFix
Silent CPU fallbackaccelerator installed but speed unchangedoperators unsupported by the back end, the provider failed to load, or the provider is missing from the listlog the node-to-provider assignment, fail on fallback in tests
Partition ping-pongmany device copies, slower than CPU alonegraph alternates between back endsmerge regions, rewrite or implement the blocking operator
Cold-start spikefirst request slowweights faulted in from disk, kernels compiled on first usewarm at startup before taking traffic
Thread oversubscriptionhigh CPU, p99 latency upmany sessions each using all cores, spinning poolsthreads per session = cores / sessions, disable spinning on shared hosts
Arena growthmemory climbs under varying shapesarena extended for the largest input and never releasedcap the GPU memory pool, shrink after bursts, bucket shapes
Stale optimized modelwrong or slow results after a library or hardware changeoffline-optimized graph saved for different providers or versionsre-optimize on upgrade, store the optimized file with the runtime version
Quantization accuracy lossoutputs drift from the float modelscales too coarse, outlier channelsper-channel scales, keep sensitive layers in higher precision, compare against the float model
Graph-replay mismatchwrong output or error after switching input sizerecorded graph used with different shapes or addressesone captured graph per shape (gpu_graph_id), copy inputs into the bound buffers

Real implementations

namewhat it isrepolicencenotes
ONNX Runtimecross-platform inference engine with execution providersmicrosoft/onnxruntimeMITthe reference for this page; docs at onnxruntime.ai
ONNXmodel format, operator definitions and IR specificationonnx/onnxApache-2.0protobuf-based file format
OpenVINOIntel's inference toolkit: converts models, compiles for Intel CPU, GPU and NPUopenvinotoolkit/openvinoApache-2.0also an ONNX Runtime provider
TensorRTNVIDIA's optimizer and runtime that builds engines per GPUNVIDIA/TensorRTApache-2.0 (open-source components)the core library ships as binaries; see the repo
LiteRT (formerly TensorFlow Lite)on-device runtime for Android, iOS and embeddedgoogle-ai-edge/LiteRTApache-2.0flat model format, delegates as back ends
ExecuTorchPyTorch's on-device runtime with ahead-of-time loweringpytorch/executorchBSD-style (see repo)plan produced before deployment
ncnnmobile inference framework without third-party dependenciesTencent/ncnnBSD-3-Clause (some third-party parts differ)focused on phones
MNNlightweight inference engine from Alibabaalibaba/MNNApache-2.0mobile and server
llama.cpp / GGMLCPU-first LLM runtime with the GGUF formatggml-org/llama.cppMITmmap'd weights, quantized kernels

Sources

Similar systems

Share the most building blocks with this one.