Rust GPU Offload Without unsafe: The 400x Mistake That Isn't Safety
Rust GPU offload now works without unsafe code. Real benchmarks: 11% faster to 46% slower than CUDA on an H100, and a transfer bug that costs 400x more.

Rust GPU offload is a compiler feature that dispatches compute to a GPU kernel without a single unsafe block, using the same ownership rules the compiler already enforces on the CPU side to manage host-device memory transfers. A new paper benchmarking this approach against hand-tuned CUDA and HIP found it lands anywhere from 11% faster to 46% slower depending on the kernel — but the single largest performance gap the paper reports, 400x, has nothing to do with safety at all. It comes from a data-transfer mistake any GPU programmer, safe or not, can make.
TL;DR
- Rust’s GPU offload work extends rustc and LLVM’s Offload infrastructure so ownership rules — not
unsafe— decide what memory crosses the host/device boundary. - On an NVIDIA H100 against hand-tuned CUDA, safe Rust kernels ranged from 11% faster to 46% slower; on an AMD MI250X against RAJA, 32% faster to 43% slower.
- The overhead is kernel-dependent, not fixed: one kernel’s worst case flipped to a 2x speedup after an algebraic rewrite, with no change to the safety model.
- The real performance trap is unrelated to safety: transferring data on every kernel launch instead of once measured over 400x slower than the optimized version.
- This targets NVIDIA (
nvptx64) and AMD (amdgcn) today, Intel is in progress, and there’s still no public crate — it’s an active Rust Project Goal, not a stable feature.
What Rust GPU offload actually is
Rust GPU offload is a compiler feature, not a library: rustc inspects whether each argument to an offloaded function is borrowed, mutably borrowed, or owned, and generates the host-device transfer code from that alone, the same way it already reasons about aliasing on the CPU. Existing paths to the GPU from Rust — FFI into the CUDA runtime, or a rust-cuda-style wrapper — put allocation, copying, and freeing device memory on the programmer, usually behind an unsafe boundary the borrow checker never reaches.
The new work, from Manuel S. Drehwald and coauthors at the University of Toronto and Lawrence Livermore National Laboratory, keeps the call site plain:
offload!(matrix_multiply,
&matrix_a, &matrix_b, &mut matrix_c
);Immutable references (&matrix_a, &matrix_b) transfer read-only, host to device. The mutable reference (&mut matrix_c) transfers back and forces a synchronization point after the kernel finishes, so the caller’s next read is guaranteed to see the GPU’s result. None of that is hand-written — it falls straight out of the ownership rules that already stop you from aliasing a mutable reference on the CPU.
This is a different sense of “offload” than the one in running a 70B model on a 4GB GPU, which streams model weights between host RAM and device memory to fit a model that doesn’t fit on-card. This offload moves compute — a kernel and its arguments — from the host to the device. Same word, opposite direction of the thing being moved.
Why this is being built at all
Rust for scientific and ML workloads has run into the same wall for years: the compute lives on GPUs, and every existing path there either locks you to one vendor’s toolchain (CUDA) or asks you to write unsafe code the compiler can’t check. A shader-based abstraction like wgpu buys portability but targets a graphics-first API, which is a mismatch for compute kernels that want to reason directly about pointers, strides, and shared memory.
Building offload into rustc sidesteps both problems at once: the same source targets NVIDIA’s nvptx64-nvidia-cuda and AMD’s amdgcn-amd-amdhsa triples by recompiling, not rewriting, and the safety comes from infrastructure the compiler already owns rather than a new unsafe surface someone has to audit.
How it works: two passes, one IR
The mechanism is a two-pass compilation pipeline built on LLVM’s Offload infrastructure rather than a bespoke Rust-only backend. The first pass walks the function rustc is asked to offload and classifies every argument by ownership: read-only, write-only, or read-write. The second pass lowers that into LLVM’s target-independent Offload IR, which already knows how to emit the actual memory-copy calls and kernel-launch sequence for whichever backend LLVM is compiling for.
That target independence is why one Rust function produces working kernels for both an H100 and an MI250X without touching either vendor’s SDK directly — the paper compiled against LLVM 23.1.0-rc1 for both targets from the same source. It also means the performance ceiling is LLVM’s Offload code generation, not something Rust-specific, which is the honest reason the benchmarks below land close to, but not always ahead of, hand-written CUDA and HIP.
The benchmarks: where safety is free, and where it costs 46%
The paper evaluates against RAJAPerf, a benchmark suite built specifically to compare HPC kernels across languages and backends, with hand-optimized CUDA and HIP as the baselines.
| GPU | Baseline | Range measured | Notes |
|---|---|---|---|
| NVIDIA H100 | Hand-tuned CUDA | 11% faster to 46% slower | FIR kernels the slowest, at 44–46% |
| AMD MI250X | RAJA (HIP) | 32% faster to 43% slower | Whole-runtime measurement |
Two things stand out. First, the range straddles zero — safe Rust beats hand-tuned CUDA on some kernels and loses on others, so “safety has a fixed tax” isn’t what the data shows. Second, the worst case the paper reports came from a finite-impulse-response (FIR) filter kernel, 44–46% slower on the H100 — until the authors rewrote the floating-point reduction to use an algebraic identity instead of the naive accumulation order, which alone produced a 2x speedup on that same kernel with no change to the offload logic. The safety model wasn’t the bottleneck; the arithmetic was.
That distinction matters if you tune kernels for LLM inference throughput or VRAM-bound serving: a compiler safety layer and a slow kernel are two separate problems, and conflating them means you spend your tuning budget on the wrong one.
The real pitfall: a 400x mistake that isn’t about safety
The paper’s largest reported gap dwarfs every safety-related number above: a naive implementation that issues a host-device data transfer on every kernel launch, instead of once for the whole run of work, measured over 400x slower than the optimized version.
This isn’t a safe-Rust-specific failure mode. The same mistake tanks hand-written CUDA or HIP code just as badly, because PCIe or NVLink transfer latency dominates almost any kernel’s actual compute time when you pay for it per call instead of once. The offload framework doesn’t prevent this mistake for you — it just makes the transfer decision explicit at the call site (owned vs. borrowed vs. mutably borrowed) instead of hidden inside a hand-written copy call you might forget to hoist out of a loop.
If you’ve diagnosed slow LLM inference caused by an unexpected host/device round trip, this will look familiar, and so will the fix: the bottleneck is rarely the compute unit itself. It’s the same lesson as tracking down a scheduler latency regression instead of assuming the CPU got slower — profile before you rewrite the part you suspect.
What this doesn’t do yet
Three limits are worth being blunt about before anyone goes looking for a cargo add:
- No public crate. This is a modified rustc and LLVM toolchain used to produce the paper’s benchmarks, not a package you can install. It’s tracked as an official Rust Project Goal for 2025H1 — active, but pre-stabilization.
- Two backends, not four.
nvptx64-nvidia-cudaandamdgcn-amd-amdhsaare supported; Intel GPU support is under development, and there is no Apple Metal target. - Function-level granularity only. The offload unit is a whole function, not a fine-grained kernel-fusion system — there’s no automatic loop fusion or scheduling across multiple offloaded calls, which is where hand-tuned CUDA still has room to win even after the arithmetic is fixed.
None of that undercuts the result. The right way to read this paper is “the safety model is close to free, and the compiler-generated transfers are competitive with hand-written ones” — not “you can replace CUDA in production today.”
Frequently asked questions
What is safe GPU offload in Rust?
It’s a compiler-level framework, built into rustc and LLVM’s Offload infrastructure, that reads whether a function argument is borrowed, mutably borrowed, or owned and generates the host-to-device and device-to-host memory transfers from that alone. No unsafe block or hand-written cudaMemcpy is needed at the call site.
Can I use Rust’s GPU offload feature today?
Not as a public crate. It’s a modified rustc and LLVM toolchain used to produce a research paper’s benchmarks, tracked as an official Rust Project Goal for 2025H1, championed by Manuel Drehwald with funding from Lawrence Livermore National Laboratory and the University of Toronto. It is active, pre-stabilization work, not something you can cargo add.
How much slower is Rust’s safe GPU offload than CUDA?
On an NVIDIA H100 against hand-tuned CUDA, measured kernels ranged from 11% faster to 46% slower. On an AMD MI250X against the RAJA/HIP baseline, the range was 32% faster to 43% slower. The overhead is kernel-dependent, not a fixed tax — one kernel’s worst case improved to a 2x speedup after an algebraic rewrite with no change to the offload logic.
What GPUs does the Rust offload framework support?
NVIDIA’s nvptx64-nvidia-cuda and AMD’s amdgcn-amd-amdhsa target triples today, compiled from an LLVM 23.1.0-rc1 base. Intel GPU support is described as under active development, and there is currently no Apple Metal target.
What does the offload! macro actually do?
It marks a function call for GPU dispatch, for example offload!(matrix_multiply, &a, &b, &mut c). Immutable references transfer read-only in one direction; a mutable reference transfers the result back and forces a synchronization point, so the caller’s next read sees the GPU’s output. All of that logic is derived from ownership, not written by hand.
What’s the biggest performance mistake in GPU offload code?
Transferring data on every kernel launch instead of batching the transfer once for a whole run of work. The paper measured this naive pattern at over 400x slower than the optimized version — a gap far larger than anything attributable to the safety model, and one that hits hand-written CUDA or HIP code just as hard.
Sources
- GPU Offload in Rust: Portable, Safe, and Fast (arXiv:2608.13759) — the benchmark numbers, target triples, and LLVM version cited throughout.
- Rust Project Goal: Expose experimental LLVM features for GPU offloading (2025H1) — ownership, timeline, and stabilization status.
- Phoronix: Offloading Rust To GPUs Proves Capable Of High Performance With Memory Safety — independent reporting corroborating the benchmark claims.
Related Articles

AI Engineering
How DNA wires a brain: 300M bits for 100 trillion connections
How DNA wires a brain: a ~10B-bit genome must wire 100 trillion connections. The scheme that closes the gap in 300M bits, and why two simpler plans fail first.

AI Engineering
Agent context compaction: keep what the 150K cutoff drops
Agent context compaction drops every block before the summary at 150K tokens. What survives, what instructions silently replaces, and the usage field that lies.

AI Engineering
Set up AI Gateway for Workers AI: one argument, every call logged
AI Gateway for Workers AI is now one argument on env.AI.run. What it logs instantly, why caching stays off until you ask, and the 60-second TTL floor.
Keep reading
Get new posts on AI, Claude Code & LLMs
New deep-dives on AI engineering, Claude Code, and developer tooling — follow along however you prefer.
About the Author
Software engineer writing about AI, Claude Code, LLMs, OpenAI, Anthropic, and developer tooling. 5+ years building production systems at Expedia Group, Tekion, and BYJU'S.