Rust↔CUDA Patterns in vision-rt
Two distinct FFI layers exist — don't mix them:
- cudarc 0.19 (workspace dep:
default-features = false, features
cuda-version-from-build-system, driver, nvrtc, std, fallback-dynamic-loading, fallback-latest) — device memory, streams, events, kernel launches. One
cudarc across the workspace, matched to kornia-rs's set so CudaStream/
CudaSlice types unify for zero-copy interop.
- trt-sys C shim (
trt_bridge.h/cpp) — TensorRT only (no C ABI exists).
New CUDA work goes through cudarc/kornia, NOT new shim functions.
Kernel authoring — kornia CudaKernel
Kernels are CUDA C strings JIT-compiled once via kornia's CudaKernel
(kornia_tensor::CudaKernel), which handles arch detection (no hardcoded sm_87)
and PTX caching. Grid math comes from vrt::cuda::{cfg_1d, cfg_2d, cfg_per_item}.
use kornia_tensor::CudaKernel;
use vrt::cuda::cfg_2d;
use cudarc::driver::{CudaSlice, DevicePtr, sys::CUdeviceptr};
const KERNELS_SRC: &str = r#"
extern "C" __global__ void my_kernel(const float* __restrict__ in, float* out, int w, int h) { ... }
"#;
// Constructor: compile the whole suite once; destructure the named functions.
let [my_kernel]: [CudaKernel; 1] =
CudaKernel::compile_many(stream.context(), KERNELS_SRC, &["my_kernel"])?
.try_into().unwrap_or_else(|_| unreachable!());
// Per-launch (async on the shared stream):
let out_raw: CUdeviceptr = out.device_ptr(stream.as_ref()).0; // CudaSlice → ptr
my_kernel
.launch_builder(&stream)
.arg(&in_slice) // CudaSlice<T>, or &raw scalar (&w_i32), or &CUdeviceptr
.arg(&out_raw)
.arg(&w_i).arg(&h_i)
.launch_cfg(cfg_2d(w, h))?; // (32,8) block, ceil-div grid
cfg_2d(w,h) for image kernels, cfg_1d(n, block) for flat arrays,
cfg_per_item(items, threads) for one-block-per-item (e.g. per keypoint).
extern "C" on every kernel — nvrtc mangles names otherwise.
__restrict__ + __ldg() for read-only inputs (helps Orin's L1/tex path).
- Compile ONCE in the constructor, never per-frame.
- Raw device pointers from TRT arrive via
TRTensorMap::get(name) →
OutputView::f32_ptr()? (dtype-checked *const f32). Cast to CUdeviceptr at
the launch site: ptr as usize as CUdeviceptr. For a CudaSlice, use
slice.device_ptr(stream).0.
Memory / stream rules
stream.alloc::<f32>(n) is unsafe (uninitialized) — fine for buffers fully
written by a kernel; use alloc_zeros when partial writes are possible.
- Copies are stream-ordered; CPU reads of a host
Vec filled by a D2H copy are
only valid after a sync. For low-latency count/scores readback use a reused
vrt::PinnedBuffer + async D2H, then one stream.synchronize() per frame.
- One shared
Arc<CudaStream> across a model's whole run (preproc + backbone +
postproc) → a single sync per frame. TRT enqueues onto it via
Session::with_stream (see vrt::model::ModelSession).
- CUDA events for timing: record with
CU_EVENT_DEFAULT, not None — cudarc's
default is DISABLE_TIMING, which makes elapsed_ms fail (a .unwrap_or(0.0)
then silently reports 0 GPU time — this bug shipped once). Prefer the
jetson-benchmarking skill's harness for reported numbers.
trt-sys shim rules (only when touching TensorRT FFI)
- Header
trt_bridge.h is pure C (opaque handles + stdint) — bindgen never sees
C++/TRT headers. Keep it that way.
- TRT 10: destroy with
delete (no ->destroy()), named-tensor API
(setTensorAddress/enqueueV3), Dims64 int64 dims.
- The Rust logger callback must be
catch_unwind-wrapped (panic across FFI is UB).
- Destruction order context → engine → runtime → logger is enforced by the Arc
chain in
vrt — never hold raw shim handles outside those wrappers.
- Off-Jetson:
TRT_STUB=1 swaps in committed bindings, no native compile/link.
FP16
Engines are built --fp16 but I/O tensors stay FP32 (TRT inserts casts).
Kernels therefore read TRT outputs as float*. Don't add half handling unless
an engine is rebuilt with FP16 I/O bindings.
1---2name: rust-cuda-patterns3description: Use when writing or modifying CUDA code from Rust in this repo — kornia CudaKernel JIT, cudarc launches/memory/events, device pointers from TRT, or the trt-sys C FFI shim. Covers this repo's conventions, not general CUDA.4---56# Rust↔CUDA Patterns in vision-rt78Two distinct FFI layers exist — don't mix them:9101. **cudarc 0.19** (workspace dep: `default-features = false`, features11 `cuda-version-from-build-system, driver, nvrtc, std, fallback-dynamic-loading,12 fallback-latest`) — device memory, streams, events, kernel launches. One13 cudarc across the workspace, matched to kornia-rs's set so `CudaStream`/14 `CudaSlice` types unify for zero-copy interop.152. **trt-sys C shim** (`trt_bridge.h/cpp`) — TensorRT only (no C ABI exists).16 New CUDA work goes through cudarc/kornia, NOT new shim functions.1718## Kernel authoring — kornia `CudaKernel`1920Kernels are CUDA C strings JIT-compiled once via kornia's `CudaKernel`21(`kornia_tensor::CudaKernel`), which handles arch detection (no hardcoded sm_87)22and PTX caching. Grid math comes from `vrt::cuda::{cfg_1d, cfg_2d, cfg_per_item}`.2324```rust25use kornia_tensor::CudaKernel;26use vrt::cuda::cfg_2d;27use cudarc::driver::{CudaSlice, DevicePtr, sys::CUdeviceptr};2829const KERNELS_SRC: &str = r#"30extern "C" __global__ void my_kernel(const float* __restrict__ in, float* out, int w, int h) { ... }31"#;3233// Constructor: compile the whole suite once; destructure the named functions.34let [my_kernel]: [CudaKernel; 1] =35 CudaKernel::compile_many(stream.context(), KERNELS_SRC, &["my_kernel"])?36 .try_into().unwrap_or_else(|_| unreachable!());3738// Per-launch (async on the shared stream):39let out_raw: CUdeviceptr = out.device_ptr(stream.as_ref()).0; // CudaSlice → ptr40my_kernel41 .launch_builder(&stream)42 .arg(&in_slice) // CudaSlice<T>, or &raw scalar (&w_i32), or &CUdeviceptr43 .arg(&out_raw)44 .arg(&w_i).arg(&h_i)45 .launch_cfg(cfg_2d(w, h))?; // (32,8) block, ceil-div grid46```4748- `cfg_2d(w,h)` for image kernels, `cfg_1d(n, block)` for flat arrays,49 `cfg_per_item(items, threads)` for one-block-per-item (e.g. per keypoint).50- `extern "C"` on every kernel — nvrtc mangles names otherwise.51- `__restrict__` + `__ldg()` for read-only inputs (helps Orin's L1/tex path).52- Compile ONCE in the constructor, never per-frame.53- **Raw device pointers from TRT** arrive via `TRTensorMap::get(name)` →54 `OutputView::f32_ptr()?` (dtype-checked `*const f32`). Cast to `CUdeviceptr` at55 the launch site: `ptr as usize as CUdeviceptr`. For a `CudaSlice`, use56 `slice.device_ptr(stream).0`.5758## Memory / stream rules5960- `stream.alloc::<f32>(n)` is unsafe (uninitialized) — fine for buffers fully61 written by a kernel; use `alloc_zeros` when partial writes are possible.62- Copies are stream-ordered; CPU reads of a host `Vec` filled by a D2H copy are63 only valid after a sync. For low-latency count/scores readback use a reused64 `vrt::PinnedBuffer` + async D2H, then one `stream.synchronize()` per frame.65- One shared `Arc<CudaStream>` across a model's whole run (preproc + backbone +66 postproc) → a single sync per frame. TRT enqueues onto it via67 `Session::with_stream` (see `vrt::model::ModelSession`).68- CUDA events for timing: record with `CU_EVENT_DEFAULT`, not `None` — cudarc's69 default is `DISABLE_TIMING`, which makes `elapsed_ms` fail (a `.unwrap_or(0.0)`70 then silently reports 0 GPU time — this bug shipped once). Prefer the71 jetson-benchmarking skill's harness for reported numbers.7273## trt-sys shim rules (only when touching TensorRT FFI)7475- Header `trt_bridge.h` is pure C (opaque handles + stdint) — bindgen never sees76 C++/TRT headers. Keep it that way.77- TRT 10: destroy with `delete` (no `->destroy()`), named-tensor API78 (`setTensorAddress`/`enqueueV3`), `Dims64` int64 dims.79- The Rust logger callback must be `catch_unwind`-wrapped (panic across FFI is UB).80- Destruction order context → engine → runtime → logger is enforced by the Arc81 chain in `vrt` — never hold raw shim handles outside those wrappers.82- Off-Jetson: `TRT_STUB=1` swaps in committed bindings, no native compile/link.8384## FP168586Engines are built `--fp16` but I/O tensors stay FP32 (TRT inserts casts).87Kernels therefore read TRT outputs as `float*`. Don't add `half` handling unless88an engine is rebuilt with FP16 I/O bindings.