A GPU feature flag that makes things slower isn’t a GPU feature — it’s a benchmark you haven’t run yet.
On August 11 we released OxiONNX 0.1.6 — a release built around one question the previous GPU work never actually answered: for this op, on this hardware, does the GPU path win or lose against the tuned CPU kernel it’s supposed to replace? Answering it honestly meant building weight and activation residency so the bus stops being the bottleneck, a direct Conv2D kernel fast enough to be worth dispatching in the first place, and a measured size gate that declines the ops which still lose even after that — plus the unrelated but overdue fix that makes wasm32 CPU inference actually execute in a browser instead of merely compiling.
No C. No C++. No ONNX Runtime binaries. No external protobuf or CUDA dependencies.
No unsafe code outside a handful of documented, audited call sites.
Just memory-safe ONNX model execution that compiles to a single static binary (or WASM) and runs everywhere — from laptops to browsers to edge devices to GPUs.
Why OxiONNX 0.1.6 is a game changer
A GPU feature flag is easy to ship and easy to get wrong in a way nobody notices, because “GPU accelerated” sounds like a strict improvement right up until someone measures it:
- Convolution and Gemm weights that never change between frames were re-uploaded to the GPU on every single dispatch — InSwapper-128 alone pushed 502.7 MB of invariant weights across the host↔device bus on every forward pass
- Elementwise ops moved bytes across the bus twice (upload, read back) at ~5 GiB/s against a CPU kernel touching them once at ~92 GB/s — some measured over an order of magnitude slower on the “accelerated” path (
Reluat 36.6x,Divat 28.6x) - Every prior “WebAssembly support” changelog entry (0.1.0, 0.1.4) meant
cargo build --target wasm32-unknown-unknownsucceeded — not that a model could run in a browser tab without panicking. It never could. - Apple’s
MLModelcompiler recompiled — and leaked — a fresh.mlmodelcinto$TMPDIRon every single process launch; one developer’s machine had 7,408 orphaned trees totaling 857 GB
OxiONNX 0.1.6 ends all of that.
- Session-lifetime GPU weight residency —
Conv/Gemmweights now cross the bus once per session, not once per dispatch (Session::gpu_resident_bytes()) - A direct implicit-GEMM Conv2D kernel replaces a CPU/GPU hybrid that measured 0.33–0.58x CPU speed — the new kernel hits ~692 GFLOP/s on M3 at InSwapper’s 128×128 decoder layer, with zero im2col materialization
- A measured two-tier size gate now declines every op that structurally loses on the GPU, closing regressions up to 36x that residency alone would have reopened
wasm32-unknown-unknownCPU inference actually works now — two independent blocking bugs (anoxifftcompile_error!on wasm32, and astd::time::Instantruntime panic with no OS clock) are both fixed- CoreML’s compile-and-leak cycle is now a persistent, content-keyed disk cache — SCRFD + ArcFace + InSwapper load in ~4.34s cold, ~0.14s warm (measured M3, 30 runs)
- 189 operators registered (204 op-type strings including aliases), verified by 3,212 passing tests
Technical Deep Dive: four layers that make the GPU path honest
- Async GPU execution (
src/session/run/sequential_async.rs,gpu_owner.rs) —Session::run_gpu_asyncis a second, smaller node-execution loop that runs the same nodes in the same order asSession::run, differing in exactly one place: a GPU dispatch is.awaited instead of blocked on, so the calling task yields to an event loop instead of blocking a thread a browser page may never have.try_gpu_dispatch_asyncis now the real dispatcher everywhere; the synchronous path is a thinpollster::block_onwrapper around it. - Session-lifetime weight residency (
oxionnx-gpu/src/context/resident.rs) —ResidentBufferscaches aTrackedBufferper graph-initializer identity for the life of theGpuContext, keyed and checked (not trusted) against the kernel slot and byte length it was uploaded for, so a key collision uploads fresh bytes instead of silently serving the wrong ones. Keyed per numeric format too, so flippingSession::set_f16_compute()mid-session is safe. - Run-scoped activation residency (
src/session/gpu_activations.rs) — the other half of the transfer problem: a GPU node’s output can now stay in its device buffer for the next GPU consumer to bind in place, precomputed per-run so a name qualifies only when every consumer can accept it resident and it isn’t a graph output. Toggle withSession::activation_residency_enabled(). - The direct Conv2D kernel (
oxionnx-gpu/src/shaders/conv2d.rs) — the im2col gather happens in-register inside the shader as the input tile stages into workgroup memory, instead of materializing a column matrixkH*kWtimes the input size and re-uploading it every call. Bias and activation fuse into the epilogue.group > 1convolutions still decline to the old hybrid path.
Getting Started
cargo add oxionnx
The core inference API is unchanged in 0.1.6:
use oxionnx::{Session, Tensor};
use std::collections::HashMap;
let session = Session::from_file("model.onnx".as_ref())?;
let mut inputs = HashMap::new();
inputs.insert("input", Tensor::new(vec![1.0, 2.0, 3.0], vec![1, 3]));
let outputs = session.run(&inputs)?;
println!("{:?}", outputs);
New in 0.1.6 — genuinely asynchronous GPU execution, awaited instead of blocked on (needs the gpu feature):
use oxionnx::{Session, Tensor};
use std::collections::HashMap;
let mut session = Session::from_file("model.onnx".as_ref())?;
let mut inputs = HashMap::new();
inputs.insert("input", Tensor::new(vec![1.0, 2.0, 3.0], vec![1, 3]));
if session.enable_gpu_async().await {
let outputs = session.run_gpu_async(&inputs).await?;
println!("{:?}", outputs);
}
What’s New in 0.1.6
- Added:
Session::run_gpu_async/enable_gpu_async(genuine async GPU execution), session-lifetime GPU weight residency, run-scoped GPU activation residency, anOxiInstanceNormfusion pass for AdaIN-style decomposed normalization, a direct implicit-GEMM Conv2D kernel, four new WGSL kernels (general-broadcast binary ops, transposed-B Gemm, PRelu, Resize), derived (not hand-written) f16 kernel variants,MlPackageModel::ensure_compiledfor up-front CoreML compilation. - Changed: CoreML
.mlpackagecompilation now cached on disk instead of redone per load; CoreML output extraction fused into a single pass (2.7x on SCRFD’s padded outputs); a measured two-tier size gate governs GPU dispatch eligibility; the five pre-existing GPU kernel families converted to the async-first, budget-checked buffer path. - Fixed:
wasm32-unknown-unknownCPU inference actually running (not just compiling) for the first time;--target wasm32-unknown-unknown --features wasm,gpunow builds; the GPU buffer pool no longer leaks device memory without bound; a GPUReduce*negative-axis bug that wrapped into a hugeusize; CPU Conv2D’s im2col workspace capped instead of scaling unbounded; a WGSLreflect-modePadbug on Vulkan/NVIDIA.
Tips
run_gpu_async/enable_gpu_asyncneed thegpufeature and are proven on native targets only so far. The async entry points and run loop are exercised bypollster::block_on-driven tests, butWasmSession(src/wasm.rs) doesn’t call any of it yet and still runsSession::runsynchronously in the browser — wiring that up and validating in a real browser is open work.- If a GPU op you expected to accelerate is now declining to CPU, that’s the size gate working as measured, not a regression.
gpu_min_transfer_elementsgates byResidencyTier: memory-bound elementwise ops never dispatch while operands still cross the bus, because the round trip costs more than the CPU kernel at any size — checkprovider_supports_opbefore assuming a bug. Session::gpu_resident_bytes()and per-runGpuRunStatsare how you confirm residency is actually helping, not just enabled —weight_cache_hits/weight_cache_misses/weight_upload_byteson a second run against the same context should show zero bytes uploaded for a previously-seen weight.- CoreML’s compile cache is content-keyed on file metadata, not a content hash — replacing a
.mlpackagein place produces a new cache entry automatically. Set$OXIONNX_COREML_CACHE_DIRif you don’t want it under$HOME/Library/Caches/oxionnx-coreml. - If you’re on a non-AVX2 x86_64 host, the 0.1.5
DFT/STFTSIMD correctness fix is still the one to check — unrelated to this release, but worth re-verifying if you haven’t upgraded past 0.1.4. f16compute is opt-in viaSession::set_f16_computeand stays within 55 dB PSNR off32on realistic tensors — resident weight bytes are exactly half when it’s on, and flipping it mid-session never serves one kernel the other’s cached bytes.
This is the foundation
OxiONNX is the ONNX inference backend across the COOLJAPAN stack — all bumped to oxionnx = "0.1.6" as part of this release:
- VoiRS — multimodal speech+vision inference
- OxiMedia — vision model deployment
- OxiWhisper — speech-model inference
- OxiGeo — geospatial ML inference
- ToRSh — model-hub inference integration
- OxiFY — LLM workflow orchestration
- OxiFace — face-model inference pipelines
- Oxi3D — 3D/graphics model inference
- OxiBonsai (
oxionnx-proto) — protobuf model parsing
Repository: https://github.com/cool-japan/oxionnx
Star the repo if you want a GPU feature flag that has to prove it’s actually faster before it gets to run.
The era of “GPU accelerated” meaning “trust us” is over.
Pure Rust ONNX inference — measured, not assumed — is here.
— KitaSan at COOLJAPAN OÜ August 11, 2026