ggmlR includes a built-in zero-dependency ONNX loader (hand-written protobuf parser in C). Load any compatible ONNX model and run inference on CPU or Vulkan GPU — no Python, no TensorFlow, no ONNX Runtime required.
Note: The examples below require a valid
.onnxmodel file. Replace"path/to/model.onnx"with the actual path on your system.
model <- onnx_load("path/to/model.onnx")
# Model summary (IR and opset version, node count, operators used)
onnx_summary(model)
# Input names and shapes (-1 for a dynamic dimension)
onnx_inputs(model)The simplest entry point is predict(). It takes an array
in the shape the ONNX file declares, with samples in the first
dimension, splits it into the batch size the model was loaded with, and
returns the output in the declared shape too:
# Random image batch, [N, C, H, W] as declared — replace with real data
x <- array(runif(8 * 3 * 224 * 224), dim = c(8L, 3L, 224L, 224L))
probs <- predict(model, x) # [8, 1000] for a classifieronnx_run()onnx_run() takes a named list (names from
onnx_inputs()) and passes each element to the model as
as.numeric(x). ONNX tensors are row-major:
the last declared axis varies fastest. An R array is column-major, so an
array with dim = c(1, 3, 224, 224) would reach the model
with its axes scrambled. Lay the data out in storage order — the
declared shape reversed — or rearrange it with aperm():
# A 224 x 224 x 3 image (H, W, C) for a model declared as [1, 3, 224, 224]
img <- array(runif(224 * 224 * 3), dim = c(224L, 224L, 3L))
# Storage order of [1, 3, 224, 224] is (W, H, C, N): W fastest
input <- aperm(img, c(2, 1, 3))
result <- onnx_run(model, list(input_name = input))The outputs come back the same way: dim describes the
buffer in storage order, the reverse of the declared shape. A detector
output declared [nbox, 4] arrives with
dim = c(4, nbox) — one box per column, so ordinary R
indexing is correct. Trailing unit axes are dropped, so an output
declared [1, 1000] arrives as a plain vector of 1000 with
no dim.
For models with multiple inputs, pass one element per input. A single token sequence (batch 1) needs no rearranging:
result <- onnx_run(model, list(
input_ids = as.numeric(tokens),
attention_mask = rep(1, length(tokens))
))By default ggmlR tries Vulkan first and falls back to CPU automatically. To force a specific backend:
# Check what's available
if (ggml_vulkan_available()) {
cat("Vulkan GPU ready\n")
ggml_vulkan_status()
}
# Load with explicit device
model_gpu <- onnx_load("path/to/model.onnx", device = "vulkan")
model_cpu <- onnx_load("path/to/model.onnx", device = "cpu")Weights are transferred to the GPU once at load time. Repeated calls
to onnx_run() do not re-transfer weights.
Some models accept variable-length inputs. Override shapes at load time:
Run in half-precision for faster GPU inference:
model_fp16 <- onnx_load("path/to/model.onnx", dtype = "f16")
result <- onnx_run(model_fp16, list(input = input))ggmlR supports 90+ ONNX operators. Every operator runs on the CPU backend; the GPU column says where it runs when a Vulkan device is available:
An operator not listed here, or a form of one the notes exclude, is
not mapped: ggmlR prints unsupported op '<name>' (or
the specific reason) to the console, and the model outputs that depend
on that node are not built — onnx_run() returns them empty.
See Debugging tips.
| Category | Operator | CPU | GPU | Notes |
|---|---|---|---|---|
| Elementwise | Add, Sub, Mul, Div | Yes | Yes | NumPy broadcasting |
| Elementwise | Mod, Pow | Yes | Yes | Pow with an integer exponent 1–8 is unrolled into multiplications |
| Elementwise | Min, Max | Yes | Yes | |
| Elementwise | Neg, Abs, Sqrt, Exp, Log, Floor, Ceil | Yes | Yes | |
| Elementwise | Sin, Cos, Erf | Yes | Yes | |
| Elementwise | Clip | Yes | Yes | |
| Activation | Relu, LeakyRelu, PRelu, Elu | Yes | Yes | |
| Activation | Sigmoid, HardSigmoid, HardSwish, Tanh | Yes | Yes | |
| Activation | Gelu, Silu | Yes | Yes | |
| Activation | Softmax | Yes | Yes | |
| Comparison / logic | Equal, Less, LessOrEqual, Greater, GreaterOrEqual | Yes | Yes | |
| Comparison / logic | Not, And, Or, Xor, Where | Yes | Yes | |
| Linear algebra | MatMul, Gemm | Yes | Yes | MatMul is batched |
| Linear algebra | Einsum | Yes | Yes | only bqc,bchw->bqhw (a batched matmul); other
equations are not mapped |
| Convolution | Conv | Yes | Yes | 1D / 2D, grouped, depthwise; auto_pad SAME_UPPER /
SAME_LOWER |
| Convolution | ConvTranspose | Yes | Yes | 1D / 2D |
| Pooling | MaxPool, AveragePool | Yes | Yes | auto_pad SAME_UPPER / SAME_LOWER |
| Pooling | GlobalAveragePool | Yes | Yes | |
| Normalization | BatchNormalization, LayerNormalization, GroupNormalization | Yes | Yes | |
| Normalization | InstanceNormalization, RMSNormalization | Yes | Yes | |
| Reduction | ReduceMean, ReduceSum, ReduceMax, ReduceMin | Yes | Yes | |
| Reduction | CumSum | Yes | Yes | |
| Shape / layout | Reshape, Flatten, Squeeze, Unsqueeze, Transpose | Yes | Yes | views or copies |
| Shape / layout | Slice, Split, Concat, Expand, Tile, Pad | Yes | Yes | |
| Shape / layout | Identity, Dropout | Yes | Yes | Dropout is the identity at inference |
| Constants | Shape, Constant, EyeLike | Yes | Host | Constant: TensorProto or scalar value_float /
value_int |
| Constants | ConstantOfShape | Yes | Host | fill value INT64, INT32, DOUBLE or FLOAT |
| Constants | Range | Yes | Yes | |
| Type | Cast | Yes | Yes | |
| Indexing | Gather, GatherND | Yes | Yes | |
| Indexing | ScatterElements | Yes | Yes* | reduction “none” or “add”; “add” on the GPU needs
VK_EXT_shader_atomic_float |
| Indexing | ScatterND | Yes | Yes | reduction “none” only |
| Indexing | TopK | Yes | Yes* | any axis up to rank 4; K within the GPU top-k pipelines; output shape depends on data, so the graph is split here |
| Resampling | Resize, Upsample | Yes | Yes* | modes “nearest” and “linear” (bilinear); “cubic” and other modes are not mapped; F32 input on the GPU |
| Quantized | QuantizeLinear, DequantizeLinear | Yes | Yes | |
| Quantized | QLinearConv, QLinearMatMul | Yes | Yes | exact integer accumulator reproducing ONNX Runtime’s int16 pair
saturation; QLinearConv takes auto_pad |
| Quantized | QLinearAdd, QLinearSigmoid, QLinearConcat | Yes | Yes | |
| Detection | NonZero | Yes | Host | output shape depends on data; the graph is split here |
| Detection | NonMaxSuppression | Yes | CPU + GPU path | output shape depends on data; the graph is split here |
| Detection | RoiAlign | Yes | CPU + GPU path | malformed ROIs or batch indices stay on the CPU kernel |
Fused pattern: RelPosBias2D (BoTNet) is not an ONNX operator. The importer recognises the relative-position-bias subgraph and replaces it with a single fused op that runs on both CPU and Vulkan.
For full working examples with real ONNX Zoo models see:
# GPU vs CPU benchmark across multiple models
# inst/examples/benchmark_onnx.R
# FP16 inference benchmark
# inst/examples/benchmark_onnx_fp16.R
# Run all supported ONNX Zoo models
# inst/examples/test_all_onnx.R
# BERT sentence similarity
# inst/examples/bert_similarity.RIf a model’s outputs come back empty or wrong:
Read the console messages. A node that cannot be
mapped does not raise an R error. ggmlR prints the node and the reason —
unsupported op '<name>' for an operator missing from
the table above, or a specific form such as
mode='cubic' not supported for Resize or an Einsum equation
other than bqc,bchw->bqhw — followed by the model
outputs that were never built because of it and the first failing node.
Those outputs come back empty from onnx_run().
onnx_summary(model)$ops lists the model’s
operators.
Check the input layout. Wrong but plausible
outputs most often mean the input reached the model with its axes
scrambled — see the row-major note in section 2, or use
predict().
Trace a divergence from ONNX Runtime. Set
ONNX_INJECT_NODES=<name>[,<name>...] and
ONNX_INJECT_DIR=<dir> to replace each named node’s
output, right after it is computed, with the reference dump
<dir>/<name>.ort.bin (flat little-endian f32).
If a later disagreement disappears, it was the earlier one amplified; if
it stays, the two are independent. Only f32 tensors are injected, and a
size mismatch is refused rather than padded or truncated.