A cudaMemcpy*Async call that returns before the copy has actually landed isn’t asynchronous — it’s a race condition wearing an async API’s clothes.
Today we released OxiCUDA 0.5.3 — a correctness release closing an async-copy race condition across oxicuda-fft’s CPU-fallback transform paths and oxicuda-memory’s DeviceBuffer::copy_from_host, where a cuMemcpy*Async/cuMemcpyHtoD_v2 call could return before the transfer had actually landed in device or host memory.
No CUDA SDK. No nvcc. No C/C++ toolchain. OxiCUDA is a type-safe, memory-safe, pure-Rust replacement for the entire NVIDIA CUDA Toolkit software stack — cuBLAS, cuDNN, cuFFT, cuSPARSE, cuSOLVER, cuRAND, and more, across 74 crates and ~1.30M lines of safe Rust. libcuda.so/nvcuda.dll is loaded dynamically at runtime via libloading; nothing about building or running OxiCUDA requires the CUDA SDK, headers, or a C/C++ compiler. The result compiles to a single static binary and runs everywhere the driver is present.
Why OxiCUDA 0.5.3 is a game changer
Async CUDA copy APIs are one of the easiest primitives in the whole ecosystem to get subtly wrong:
cuMemcpy*Asyncreturns once the driver has queued the transfer, not once it has landed — conflating the two is one of the most common correctness bugs in hand-rolled CUDA code, pure-Rust or otherwise- Every OxiCUDA
Streamis created withCU_STREAM_NON_BLOCKING, which by definition does not implicitly synchronize with the legacy default stream — so a copy queued on one stream and read from another can race even when the “obvious” CUDA intuition says it shouldn’t - The failure is timing-dependent: it only shows up under load, on a busy GPU, or with the “wrong” transfer latency for a given host/device pair — which makes it look like a flake, not a bug, and CI runs on a quiet GPU can pass for months
- None of this crashes. A silently all-zero FFT transform or a device buffer read before its upload lands just produces a wrong, plausible-looking answer
OxiCUDA 0.5.3 ends all of that.
oxicuda-fft’s shared copy helpers now actually wait.copy_dtoh_async/copy_htod_async— used bytransforms::c2c,c2r,fft2d,fft3d, andr2calike — enqueuedcuMemcpyDtoHAsync/cuMemcpyHtoDAsyncbut returned immediately, before the copy completed.dst(an ordinary pageableVec) could still be mid-transfer when the very next line read it for the host-fallback DFT, orsrccould be dropped before the upload landed. Both helpers now callstream.synchronize()before returning, closing the race for all five transform kernels at once since they share the same two functions.DeviceBuffer::copy_from_hostnow finishes what it starts.cuMemcpyHtoD_v2only blocks until the host source is staged into the driver’s DMA buffer — not until the transfer to device memory itself completes.copy_from_hostnow also callscuCtxSynchronize, mirroring the existing behavior ofzeroed, so a kernel issued right aftercopy_from_hoston a non-blocking stream can no longer observe pre-upload zeros.- No new API, no migration. Both fixes are internal — every existing call site gets the correctness guarantee automatically on upgrade.
Technical Deep Dive
- Foundation —
oxicuda-memory.DeviceBuffer::copy_from_hostis this release’s second fix, and it’s not a rarely-used corner of the API: it’s the exact call every quick-start example uses to get data onto the device in the first place. - FFT transforms —
oxicuda-fft.transforms::c2c,c2r,fft2d,fft3d, andr2call route through the same sharedcopy_dtoh_async/copy_htod_asyncpair, so one fix in two functions closes the race across the entire transform family simultaneously. - Streams — the non-blocking-by-default design. Every OxiCUDA
StreamisCU_STREAM_NON_BLOCKINGfrom construction, which is precisely why the driver’s own implicit default-stream synchronization — the safety net CUDA newcomers often rely on without realizing it — doesn’t save you here. OxiCUDA’s copy helpers now provide that guarantee explicitly instead. - The wider domain layer.
oxicuda-blas,oxicuda-dnn,oxicuda-sparse,oxicuda-solver, and the dozens of domain crates built onoxicuda-memory/oxicuda-fftall inherit both fixes today, without a single line of their own code changing.
Getting Started
cargo add oxicuda --features blas
Default features remain driver, memory, and launch; every subsystem — ptx, autotune, blas, dnn, fft, sparse, solver, rand, nvrtc, primitives, vulkan, metal, webgpu, rocm, level-zero, and full for everything — is its own opt-in flag.
A complete GEMM, end to end. Look closely at the copy_from_host calls below — that’s exactly the function this release fixed:
use oxicuda::prelude::*;
fn main() -> Result<(), oxicuda::Error> {
// Initialize driver and select GPU device
let device = Device::get(0)?;
let ctx = Context::new(device)?;
let stream = Stream::new(&ctx)?;
// Allocate device memory
let mut d_a = DeviceBuffer::<f32>::zeroed(1024)?;
let mut d_b = DeviceBuffer::<f32>::zeroed(1024)?;
let mut d_c = DeviceBuffer::<f32>::zeroed(1024)?;
// Copy host data to device -- now correctly synchronized before returning
d_a.copy_from_host(&host_a)?;
d_b.copy_from_host(&host_b)?;
// Launch a GEMM: C = alpha * A @ B + beta * C
let handle = BlasHandle::new(&stream)?;
handle.gemm(
Transpose::None, Transpose::None,
m, n, k,
1.0f32, // alpha
&d_a, lda,
&d_b, ldb,
0.0f32, // beta
&mut d_c, ldc,
)?;
stream.synchronize()?;
// Copy result back to host
let mut result = vec![0.0f32; m * n];
d_c.copy_to_host(&mut result)?;
Ok(())
}
On a machine with an NVIDIA GPU, run the same validation this release’s fixes were checked against:
cargo test --features gpu-tests
What’s New in 0.5.3
oxicuda-fftasync-copy race fixed.copy_dtoh_async/copy_htod_async(shared byc2c,c2r,fft2d,fft3d,r2c) now callstream.synchronize()before returning, instead of returning while the copy is still in flight.oxicuda-memory::DeviceBuffer::copy_from_hostrace fixed. Now also callscuCtxSynchronizeaftercuMemcpyHtoD_v2, matchingzeroed’s existing synchronous guarantee.- No API changes. Both fixes are internal to existing functions — no migration needed.
- Test suite held at 38,675 passing tests (
--all-features; 37,320 with default features) — this release is a correctness fix, not a feature addition.
Tips
- Upgrade if you’ve ever seen an intermittent all-zero or garbage result under GPU load that didn’t reproduce reliably. That symptom — works most of the time, fails under load, never on a quiet GPU — is the signature of exactly this class of race. 0.5.3 fixes it outright.
- Remove any manual
stream.synchronize()workaround you added aftercopy_from_host. It’s no longer necessary — the function now synchronizes internally. - Don’t assume the legacy default stream’s implicit synchronization will save you. Every OxiCUDA
StreamisCU_STREAM_NON_BLOCKING, which by design skips that implicit sync — this is exactly the property that made the pre-0.5.3 race possible. - If you maintain your own async copy helpers on top of
oxicuda-driver, audit them for the same “returns after enqueue, not after completion” pattern this release fixed — it’s an easy mistake to repeat in custom code. - Run
cargo test --features gpu-testson real hardware under load, not just idle, if you want to reproduce timing-dependent races like this one yourself — an idle GPU can mask exactly this kind of bug for a long time.
This is the foundation
OxiCUDA is the GPU layer beneath the rest of the COOLJAPAN ecosystem. Its own architecture diagram puts SciRS2, OxiONNX, TrustformeRS, and ToRSh directly on top of it, with OxiBLAS and OxiFFT rounding out the list of projects that lean on this stack — and the oxicuda crate itself is an umbrella re-export over all 74 crates, so a single cargo add oxicuda pulls in whichever subsystem a dependent project needs. Every one of them inherits 0.5.3’s correctness fixes today, without a single line of their own code changing.
Repository: https://github.com/cool-japan/oxicuda
Star the repo if you believe an “async” copy should mean the data is actually there when the call returns to your control flow. Every star tells us to keep building.
The era of silent GPU data races is over. Pure Rust GPU computing is here — and as of 0.5.3, its async copies actually finish before they say they did.
— KitaSan at COOLJAPAN OÜ July 27, 2026