Backend
Understand backend engines, PJRT pluggable plugins, and execution environments.
Overview
GoMLX is a Machine Learning focused API, that “lowers” computations into a standard portable internal GO API defined in compute.Backend.
If you are simply using GoMLX, you don’t really need to worry about the backend API, you just need to know that you can select, based
on where you want to execute your models (CUDA, CPU, using pure Go). Simply create a backend at program startup with compute.New() and
reuse it throughout the application lifetime.
Configuring the Execution Environment
Most programs will simply use compute.New() to create a backend in a program, or testutil.BuildTestBackend() for tests.
Both default to the “best” backend available (out of the linked in backends).
You can configure which backend to use and specify options in two ways: programmatically using the
compute.NewWithConfig(config) function or setting the environment variable
GOMLX_BACKEND, which will be used by compute.New().
Example config (or GOMLX_BACKEND) values:
go: Forces the pure Go backend.xla: Uses the XLA backend. It will attempt to use one of TPU, CUDA (Nvidia GPU), or CPU in that order. You can also specify which XLA plugin to use explicitly: E.g.:xla:cpu,xla:cuda.onnx: Uses the ONNX Runtime backend (included bybackends/defaultwhen built with-tags=onnx, or by importinggithub.com/gomlx/compute-onnx).
See below for specific backend configurations.
Supported Backends
1. The XLA Backend ("xla")
This is the default and highest-performance backend. It calls XLA (the compiler powering JAX, TensorFlow, and PyTorch/XLA) to compile graphs to optimized machine code.
- Pros: Incredibly fast, supports GPU (CUDA) and TPU execution, performs operator fusion and memory optimizations automatically.
- Cons: Relies on CGO (which requires C/C++ dependencies) and currently only supports static shapes (compilation is tied to fixed input dimensions).
The xla plugin expects a config string in the format xla:<plugin>[,<option>=<value>].... Where <plugin> can be set to “cpu”, “cuda” or “tpu” or
the path for the PJRT plugin (see below) to use. The following options are supported:
Configuration options:
tf32(boolean, default=true): controls whether to use TF32 for DotGeneral operations that are using float32 (it can be faster in modern GPUs). It’s enabled by default.shared_buffer(boolean, default=true): controls whether to use shared buffers for the device buffer (where device=CPU). It’s enabled by default if the plugin is called “cpu”.preallocate(boolean, default=false): whether the CUDA PJRT preallocates a large portion of the memory.memory_fraction(float, default=0.75): how much memory to preallocate, if preallocate=true. CUDA only.allocator(string, default=“default”): which allocator to use. For CUDA the available ones are “default” (== “bfc”), “bfc” (“best-fit for coalescing”, avoids framementation), “cuda_async” (dynamic, no preallocation), “platform” (slow, good for debugging), “vmm”. CUDA only.visible_devices(list of integers, e.g., “0;1;2”): list IDs of the devices made visible to the backend.use_tfrt_gpu_client(boolean, default=false): uses the “TFRT” dispatcher for GPU.
Example:
GOMLX_BACKEND=xla:cuda,preallocate=true: Use XLA CUDA, and preallocate 75% (the default) for faster memory management for this session.
The PJRT plugins also read the XLA_FLAGS environment variable for additional lower-level configurations. Set XLA_FLAGS=--help and
it will return an error with the messages.
XLA’s Pluggable PJRT Plugin Architecture
XLA uses a “plugin” model, where it defines a standard C API called PJRT (“Pretty much Just another RunTime”) and a language (StableHLO) to express the computation and there are plugins (sometimes closed source) that implement them.
If you are only using GoMLX, you don’t need to know this, but if you are curious, this is how it looks:
graph TD
GoMLX["GoMLX App"] --> ComputeAPI["compute.Backend Interface"]
ComputeAPI --> XLA["xla Backend (CGO)"]
XLA --> PJRTLoader["PJRT Dynamic Linker"]
PJRTLoader -->|Load plugin.so| CPU["CPU PJRT Plugin"]
PJRTLoader -->|Load plugin.so| CUDA["CUDA (Nvidia) PJRT Plugin"]
PJRTLoader -->|Load plugin.so| TPU["TPU (Google Cloud) PJRT Plugin"]PJRT plugins are dynamically loaded libraries (.so on Linux, .dylib on macOS, .dll on Windows). There is typically one plugin per target hardware accelerator.
PJRT Auto-Installation
To simplify the developer experience, GoMLX includes an auto-installer. At startup, the xla package checks if a compatible PJRT plugin is installed. If not, it downloads and caches the required binaries locally in:
- Linux:
~/.local/lib/go-xla/ - macOS:
~/Library/Application Support/go-xla/ - Windows:
~\AppData\Local\go-xla\
Disabling Auto-Installation
For offline deployment or custom production builds (like Docker images), auto-installation can be disabled:
- Via Environment Variable: Set
GOMLX_NO_AUTO_INSTALL=1. - Programmatically: Call
xla.EnableAutoInstall(false)before initializing the backend.
2. The Go Backend ("go")
A pure Go implementation of the compute API. It does not use CGO or C++ libraries.
- Pros: 100% portable. It compiles easily to WebAssembly (WASM) and runs in the browser, making it possible to deploy models on client-side web apps.
- Cons: Slower than XLA for heavy model training.
- Performance Enhancements:
- SIMD support: Utilizes Go 1.26’s experimental
simd/archsimdpackage (AVX2/AVX512) for high-performance matrix multiplications (matmul). - Fused Operations: Implements fused activation and layer operations to minimize memory allocation.
- Quantization: Supports quantized operations for faster inference on smaller memory footprints.
- SIMD support: Utilizes Go 1.26’s experimental
It accepts the following special environment variables for tuning:
GOMLX_SIMD_AVX512: Set to0orfalseto disable AVX512 SIMD vectorization.GOMLX_SIMD_AVX2: Set to0orfalseto disable AVX2 SIMD vectorization.GOMLX_FUSION: Set to0orfalseto disable fused operations.
It’s relatively easy to add specialized fused operations, or SIMD versions for specific CPUs. Open an issue in the GoMLX repo, or reach use out in our slack channel for questions.
3. The Darwin ML Backend ("go-darwinml")
(Experimental) Implements bindings to Apple’s native CoreML and Metal Performance Shaders (MPSGraph) runtimes.
- Pros: Leverages Apple Silicon’s Apple Neural Engine (ANE) and unified memory GPU (Metal) on Macs.
4. The ONNX Runtime Backend ("onnx")
Uses ONNX Runtime (ORT) to execute GoMLX computation graphs on Linux and Windows (amd64). Implemented in package github.com/gomlx/compute-onnx.
- Pros: Interoperability with the ONNX ecosystem, ability to execute ONNX models, and support for exporting trained GoMLX models to the standard
.onnxfile format. - Cons: Requires CGO/C++ dependencies for ONNX Runtime. Currently supports Linux/amd64 and Windows/amd64.
- Importing: Included in
github.com/gomlx/gomlx/backends/defaultwhen building with-tags=onnx. Alternatively, importgithub.com/gomlx/compute-onnxdirectly.
Configuration options (via GOMLX_BACKEND=onnx:<options>):
cpu: Forces CPU execution.cuda(orgpu): Forces CUDA GPU execution via the ONNX Runtime CUDA Execution Provider.<path/to/libonnxruntime.so>: Explicit path to the ONNX Runtime shared library binary (.so,.dylib, or.dll), bypassingONNXRUNTIME_SHARED_LIBRARY_PATH.log=<level>: Sets internal logging severity level (0=Error, 1=Warning, 2=Info, 3=Verbose).- Default (empty): Auto-detects if an NVIDIA GPU is available and defaults to CUDA if present, falling back to CPU.
Example: GOMLX_BACKEND="onnx:cuda,log=2" or GOMLX_BACKEND="onnx:/path/to/libonnxruntime.so"
ONNX Runtime Auto-Installation
The ONNX Runtime backend includes an auto-installer. At startup, if the ONNX Runtime shared library (libonnxruntime.so on Linux, libonnxruntime.dylib on macOS, onnxruntime.dll on Windows) is not found, it automatically downloads and extracts the official ONNX Runtime binaries locally into:
- Linux:
~/.local/lib/onnxruntime/ - macOS:
~/Library/Application Support/onnxruntime/ - Windows:
~\AppData\Local\onnxruntime\
Custom Library Path & Disabling Auto-Installation
- Custom Library Path: Specify an explicit shared library location by passing a path in the configuration string (e.g.
GOMLX_BACKEND=onnx:/path/to/libonnxruntime.so, which bypassesONNXRUNTIME_SHARED_LIBRARY_PATH), or by setting theONNXRUNTIME_SHARED_LIBRARY_PATHenvironment variable. - Disabling Auto-Installation: Set environment variable
GOMLX_NO_AUTO_INSTALL=1or callonnxbackend.EnableAutoInstall(false)programmatically before initializing the backend to prevent automatic downloads (ideal for offline environments or production Docker builds). - Standalone Installer Utility: You can pre-install libraries using the CLI tool in
github.com/gomlx/compute-onnx/cmd/onnxruntime_installer.
Debugging & Saving Models on Compilation Failure
- Save Model on Failure: If graph compilation or session creation fails, set environment variable
GOMLX_ONNX_SAVE_ON_FAILURE=/path/to/failed_model.onnx. When set, the backend will write the serialized ONNX model protobuf to that file path for inspection and print aklognotification.
Exporting / Saving Models to .onnx Format
With the onnx backend enabled, you can save trained GoMLX models to standard .onnx files. These files can then be loaded and executed with ONNX Runtime in GoMLX or deployed in other languages and inference engines.
- Package:
github.com/gomlx/gomlx/ml/model/onnx(protected by build tag//go:build onnx). - Key Functions:
onnx.SaveToFile(backend, exec, filePath, inputShapes, inputNames, outputNames): Exports the computation graph and model parameters to an.onnxfile.onnx.Save(backend, exec, writer, inputShapes, inputNames, outputNames): Exports the ONNX model to anio.Writer.onnx.LoadFromFile(backend, filePath)/onnx.Load(backend, reader): Loads an.onnxmodel into an executable for inference within GoMLX.onnx.IsONNX(backend): Returnstrueif the providedcompute.Backendis an ONNX backend instance (*onnxbackend.Backend).
- Dynamic Axes: Supports dynamic input dimensions (such as variable batch sizes) using
exec.WithDynamicAxes(...)andshapes.MakeDynamic(...). - Example: See the
save_onnx.godemo in the UCI-Adult example (build with-tags=onnx).
WebAssembly / Browser Execution (ONNX Runtime Web)
The onnx backend also supports compiling to WebAssembly (GOOS=js GOARCH=wasm) and executing inside web browsers via ONNX Runtime Web.
- Supported Execution Providers:
webgpu(orgpu): Hardware-accelerated GPU shader execution via WebGPU (best for large vision/transformer models and parallel batches).wasm(orcpu): High-speed CPU WebAssembly execution using SIMD instructions (best latency for small models and single-sample loops).webnn: Hardware NPU/GPU acceleration via the experimental Web Neural Network API in Chromium.
- Auto-Detection: Automatically detects WebGPU hardware availability at startup and defaults to
webgpuif present, otherwise falling back towasmCPU. - Zero-Config Script Loading: If
ort.min.jsis not embedded in the page, the backend automatically injects it from the official CDN at runtime.
Inspecting & Visualizing .onnx Files
- CLI Printer (
onnx_printer): To inspect the contents, shapes, initializers, and operations of a.onnxmodel file directly in the terminal, use theonnx_printerutility ingithub.com/gomlx/compute-onnx/cmd/onnx_printer:It formats tensor shapes using GoMLXgo run github.com/gomlx/compute-onnx/cmd/onnx_printer path/to/model.onnxshapes.Shape(including dynamic dimension names), prints operations on a single line per op, and truncates large constant tensors (use-max_itemsor-nto control element limit). - Graphical Visualization: For interactive graphical diagram visualization of ONNX computation graphs, open your
.onnxmodel file using Netron.
Devices and DeviceNum
A backend can be connected to multiple accelerator devices (for instance, a machine with multiple GPUs or a TPU pod). To address specific devices within a backend, GoMLX uses the compute.DeviceNum type (which is an integer wrapper).
- Single-Device Default: If you are not using multiple accelerators, you can always simply default this device number to
0. - Addressing Devices: For multi-device setups, device numbers range from
0tobackend.NumDevices() - 1. You specify this number when allocating buffers on specific devices (e.g., intensors.FromShapeForBackend), performing distributed computations, or pinning executions.
There is support for distributed execution, including distributed datasets and distributed training, see packages compute/distributed, gomlx/core/tensors/dtensor along with the standard packages to train models.
Backend Compliance Testing
For anyone wanting to develop a new backend.
To ensure different backend engines behave identically and yield mathematically correct results, GoMLX includes a compliance test suite in support/backendtest.
If you write a custom backend, you can run all compliance checks by referencing this package in your test file:
package mybackend_test
import (
"testing"
"github.com/gomlx/compute/support/backendtest"
)
func TestCompliance(t *testing.T) {
// Run all official compliance tests against your backend
backendtest.RunAll(t, myBackend)
}
Compliance tests automatically check backend capabilities. Any tests that require operations your backend does not yet implement are gracefully skipped.