Skip to main content

tenferro_gpu/cubecl/raw/
mod.rs

1//! Type-safe raw CUDA extension session (issue #1597).
2//!
3//! [`Session`] is the typed context capability exposed by
4//! [`CudaExecSession::with_raw`](super::exec_session::CudaExecSession::with_raw):
5//! while a raw session is alive the tenferro primary context is activated on
6//! the current thread with a single captured execution stream bound.
7//!
8//! Safety model
9//! ------------
10//!
11//! - `Session`, [`StreamRef`], [`TensorRef`], [`DeviceBytes`], [`Module`], and
12//!   [`Function`] are all scoped to the request lifetime and are `!Send +
13//!   !Sync`, so the thread-local context/stream capability cannot migrate to
14//!   another thread.
15//! - `StreamRef` is not a `u64` and does not expose its native handle in safe
16//!   code; vendor FFI receives the handle only through an explicit unsafe
17//!   escape that must not retain it past the session scope.
18//! - `TensorRef` carries a validated device span and ownership identity; it
19//!   provides no `Deref` to the device buffer.
20//! - A mutable device access can only be produced through [`Session::tensor_mut`]
21//!   which requires an exclusive `&mut` borrow of a compatibly-typed tensor, or
22//!   through a freshly allocated [`Session::alloc_output`] tensor.
23//! - A raw kernel launch is an `unsafe fn` (see [`Session::launch`]) whose
24//!   ABI, argument order, alias, and liveness invariants are the caller's
25//!   contract.
26
27mod module;
28mod nvrtc;
29
30use std::fmt;
31use std::marker::PhantomData;
32use std::rc::Rc;
33
34use tenferro_tensor::{TensorRank, TypedTensor};
35
36use super::runtime::CudaRuntime;
37use super::{CudaExtensionCache, CudaExtensionCacheGuard, CudaRuntimeIdentity};
38
39pub use nvrtc::NvrtcOptions;
40
41/// A loaded CUDA module (PTX or CUBIN) on the tenferro primary context.
42///
43/// Retains the tenferro `CudaRuntime` so the context/device stays alive as
44/// long as the module exists. Every [`Function`] derived from it retains the
45/// same inner handle (`Rc`), so the image is never unloaded while a function
46/// handle is still in use. Dropping the last strong reference unloads the
47/// module.
48///
49/// `Module` is `!Send + !Sync` (like the rest of the raw seam), matching the
50/// session-scoped execution authority of issue #1597.
51#[allow(dead_code)]
52// `runtime` is never dereferenced: holding the `CudaRuntime` Arc keeps the
53// tenferro primary context alive for the module's whole lifetime, which is
54// the load-bearing effect this field provides.
55pub struct Module {
56    inner: Rc<ModuleInner>,
57    runtime: CudaRuntime,
58    _not_send_sync: PhantomData<Rc<()>>,
59}
60
61/// Shared inner handle shared between a [`Module`] and its [`Function`]s.
62///
63/// Retains the tenferro primary context identity so unload re-activates the
64/// correct context on the (same, session-scoped) thread before the driver
65/// frees the image. `Module`/`Function` are `!Send + !Sync`, so this drop
66/// never races a foreign current-context.
67pub(crate) struct ModuleInner {
68    handle: cudarc::driver::sys::CUmodule,
69    context: cudarc::driver::sys::CUcontext,
70}
71
72impl Drop for ModuleInner {
73    fn drop(&mut self) {
74        // SAFETY: `handle` came from `cuModuleLoadData` under the tenferro
75        // primary context and is not yet unloaded; every `Function` that
76        // could still reference it retains an `Rc<ModuleInner>`. The module
77        // is `!Send + !Sync`, so this drop runs on the session thread.
78        unsafe {
79            let was_current = cudarc::driver::result::ctx::get_current().ok();
80            let _ = cudarc::driver::result::ctx::set_current(self.context);
81            let _ = cudarc::driver::result::module::unload(self.handle);
82            if let Some(previous) = was_current.flatten() {
83                if previous != self.context {
84                    let _ = cudarc::driver::result::ctx::set_current(previous);
85                }
86            }
87        }
88    }
89}
90
91impl Module {
92    /// Wrap a freshly loaded driver module handle, retaining `runtime` so the
93    /// primary context stays alive for the module's lifetime.
94    pub(crate) fn from_handle(
95        op: &'static str,
96        handle: cudarc::driver::sys::CUmodule,
97        runtime: CudaRuntime,
98    ) -> crate::Result<Self> {
99        if handle.is_null() {
100            return Err(crate::Error::backend_source(
101                op,
102                std::io::Error::other("driver returned a null module handle"),
103            ));
104        }
105        let context = runtime.primary_context();
106        Ok(Self {
107            inner: Rc::new(ModuleInner { handle, context }),
108            runtime,
109            _not_send_sync: PhantomData,
110        })
111    }
112
113    /// Look up a kernel entry point by name.
114    ///
115    /// The returned [`Function`] retains this module, so the module cannot be
116    /// unloaded while the function is alive.
117    ///
118    /// # Errors
119    ///
120    /// Returns the driver's typed error via [`crate::Error::BackendSource`]
121    /// when the symbol does not exist in the module.
122    pub fn function(&self, name: &str) -> crate::Result<Function> {
123        module::module_function(&self.inner, name, "module.function")
124    }
125
126    /// Return the CUDA module handle (for internal bridge and test use).
127    #[allow(dead_code)]
128    pub(crate) fn handle(&self) -> cudarc::driver::sys::CUmodule {
129        self.inner.handle
130    }
131}
132
133/// A handle to a kernel within a loaded [`Module`].
134///
135/// Retains the owning module inner (`Rc`), so the underlying function handle
136/// is valid for as long as this handle lives and the module is not unloaded
137/// mid-flight.
138#[allow(dead_code)]
139// `module` is never dereferenced: the retained `Rc<ModuleInner>` prevents
140// module unload while a queued kernel may still reference the function handle,
141// which is the load-bearing effect this field provides.
142pub struct Function {
143    handle: cudarc::driver::sys::CUfunction,
144    module: Rc<ModuleInner>,
145}
146
147impl Function {
148    /// Return the raw function handle (for internal launch use).
149    pub(crate) fn handle(&self) -> cudarc::driver::sys::CUfunction {
150        self.handle
151    }
152}
153
154/// Launch geometry for one raw CUDA kernel.
155///
156/// `grid` is the thread-block grid `[x, y, z]`; `block` is the per-block
157/// thread dimensions `[x, y, z]`; `shared_mem_bytes` is the dynamic
158/// shared-memory budget per block.
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct LaunchConfig {
161    /// Grid dimensions `[x, y, z]`.
162    pub grid: [u32; 3],
163    /// Block dimensions `[x, y, z]`.
164    pub block: [u32; 3],
165    /// Dynamic shared-memory bytes per block.
166    pub shared_mem_bytes: u32,
167}
168
169impl LaunchConfig {
170    /// A flat one-dimensional launch of `threads` threads in blocks of `block`.
171    ///
172    /// # Errors
173    ///
174    /// Returns [`crate::Error::Validation`] carrying
175    /// `ValidationError::InvalidArgument` when `block == 0`.
176    pub fn flat(threads: u32, block: u32, shared_mem_bytes: u32) -> crate::Result<Self> {
177        if block == 0 {
178            return Err(crate::Error::invalid_argument(
179                "launch_config.flat",
180                "block",
181                "block size must be non-zero",
182            ));
183        }
184        let grids = threads.div_ceil(block);
185        Ok(Self {
186            grid: [grids.max(1), 1, 1],
187            block: [block, 1, 1],
188            shared_mem_bytes,
189        })
190    }
191}
192
193/// Raw CUDA extension session.
194///
195/// Not constructible by users. Represents "tenferro primary context active on
196/// this thread with one captured execution stream bound". `!Send + !Sync` by
197/// construction (`Rc`).
198pub struct Session<'s> {
199    runtime: CudaRuntime,
200    cache: &'s CudaExtensionCache,
201    stream: u64,
202    _not_send_sync: PhantomData<Rc<()>>,
203    _scope: PhantomData<&'s ()>,
204}
205
206impl<'s> Session<'s> {
207    /// Create a raw session bound to `runtime`, `cache`, and the current
208    /// CubeCL stream.
209    ///
210    /// # Safety
211    ///
212    /// Caller must have activated the tenferro primary context for `runtime`
213    /// and hold it stronger than `'s`; `stream` must be the captured CubeCL
214    /// stream bound to the current thread.
215    pub(crate) unsafe fn new(
216        runtime: CudaRuntime,
217        cache: &'s CudaExtensionCache,
218        stream: u64,
219    ) -> Self {
220        Self {
221            runtime,
222            cache,
223            stream,
224            _not_send_sync: PhantomData,
225            _scope: PhantomData,
226        }
227    }
228
229    /// Return the identity of the runtime backing this session.
230    pub fn runtime_identity(&self) -> CudaRuntimeIdentity {
231        self.runtime.runtime_identity()
232    }
233
234    /// Block the host until work enqueued on the session's stream completes.
235    ///
236    /// This is the only host barrier on the raw-session success path; regular
237    /// successes only enqueue.
238    ///
239    /// # Errors
240    ///
241    /// Returns [`crate::Error::BackendSource`] when the CUDA synchronize call
242    /// fails.
243    pub fn synchronize(&self) -> crate::Result<()> {
244        self.runtime.synchronize()
245    }
246
247    /// Borrow the captured execution stream.
248    pub fn stream(&self) -> StreamRef<'s> {
249        StreamRef {
250            raw: self.stream,
251            _scope: PhantomData,
252            _not_send_sync: PhantomData,
253        }
254    }
255
256    /// Build a checked read-only device reference for a GPU-backed tensor.
257    ///
258    /// The returned reference is tied to both this session borrow and the
259    /// tensor borrow, so the span cannot outlive the tensor's allocation.
260    ///
261    /// # Errors
262    ///
263    /// Returns [`crate::Error::RuntimeState`] when the tensor is not resident on
264    /// this session's runtime/device.
265    pub fn tensor<'a, T>(
266        &'a self,
267        tensor: &'a TypedTensor<T, impl TensorRank>,
268    ) -> crate::Result<TensorRef<'a, T>>
269    where
270        T: 'static,
271    {
272        super::dispatch::ensure_resident_on_runtime(&self.runtime, tensor, "raw.tensor")?;
273        let prepared = super::dispatch::cubecl_buffer(tensor, "raw.tensor")?;
274        let byte_len = prepared.byte_len;
275        let resource = self
276            .runtime
277            .client()
278            .get_resource(prepared.handle().clone())
279            .map_err(|err| crate::Error::backend_source("raw.tensor", err))?;
280        let base =
281            super::interop::cuda_device_ptr_from_addr(resource.resource().ptr, "raw.tensor")?;
282        Ok(TensorRef {
283            base,
284            byte_len,
285            _scope: PhantomData,
286            _dtype: PhantomData,
287            _not_send_sync: PhantomData,
288        })
289    }
290
291    /// Build a checked mutable device reference for a GPU-backed tensor.
292    ///
293    /// Requires an exclusive borrow of the tensor, so aliasing mutable device
294    /// access cannot be produced from a shared `&TypedTensor<T>`. The returned
295    /// reference is tied to both this session borrow and the tensor borrow, so
296    /// the span cannot outlive the tensor's allocation.
297    ///
298    /// # Errors
299    ///
300    /// Returns [`crate::Error::RuntimeState`] when the tensor is not resident on
301    /// this session's runtime/device.
302    pub fn tensor_mut<'a, T>(
303        &'a self,
304        tensor: &'a mut TypedTensor<T, impl TensorRank>,
305    ) -> crate::Result<TensorMut<'a, T>>
306    where
307        T: 'static,
308    {
309        super::dispatch::ensure_resident_on_runtime(&self.runtime, tensor, "raw.tensor_mut")?;
310        let prepared = super::dispatch::cubecl_buffer(tensor, "raw.tensor_mut")?;
311        let byte_len = prepared.byte_len;
312        let resource = self
313            .runtime
314            .client()
315            .get_resource(prepared.handle().clone())
316            .map_err(|err| crate::Error::backend_source("raw.tensor_mut", err))?;
317        let base =
318            super::interop::cuda_device_ptr_from_addr(resource.resource().ptr, "raw.tensor_mut")?;
319        Ok(TensorMut {
320            base,
321            byte_len,
322            _scope: PhantomData,
323            _dtype: PhantomData,
324            _not_send_sync: PhantomData,
325        })
326    }
327
328    /// Allocate a dense GPU tensor of `T` on the session's device.
329    ///
330    /// # Errors
331    ///
332    /// Returns [`crate::Error::Validation`] on shape overflow or
333    /// [`crate::Error::BackendSource`] on allocation failure.
334    pub fn alloc_output<T>(&self, shape: &[usize]) -> crate::Result<TypedTensor<T>>
335    where
336        T: cubecl::prelude::CubeElement
337            + tenferro_tensor::TensorScalar
338            + Clone
339            + Send
340            + Sync
341            + 'static,
342    {
343        super::dispatch::alloc_output(&self.runtime, shape)
344    }
345
346    /// Retain a clone of a resident tensor's CubeCL allocation handle.
347    ///
348    /// The returned [`DeviceBytes`] holds a reference-counted clone of the
349    /// tensor's allocation handle, so the device memory stays alive until the
350    /// guard is dropped. Use this when a vendor library enqueues asynchronous
351    /// work against the tensor's address and, on a failed synchronization
352    /// barrier, the guard must be intentionally forgotten so allocation
353    /// reclamation cannot race an in-flight kernel.
354    ///
355    /// # Errors
356    ///
357    /// Returns [`crate::Error::RuntimeState`] when `tensor` is host-backed,
358    /// belongs to a non-CubeCL backend family, belongs to a different CUDA
359    /// runtime domain, or is not resident on this session's device, or
360    /// [`crate::Error::BackendSource`] when CubeCL cannot inspect the retained
361    /// resource.
362    ///
363    /// # Examples
364    ///
365    /// ```
366    /// use tenferro_gpu::cuda::raw::{DeviceBytes, Session};
367    /// use tenferro_tensor::TypedTensor;
368    ///
369    /// fn check<'s>(
370    ///     raw: &Session<'s>,
371    ///     tensor: &TypedTensor<f32>,
372    /// ) -> tenferro_tensor::Result<DeviceBytes<'s>> {
373    ///     raw.retain_tensor(tensor, "test.retain_tensor")
374    /// }
375    /// ```
376    pub fn retain_tensor<T>(
377        &self,
378        tensor: &TypedTensor<T, impl TensorRank>,
379        op: &'static str,
380    ) -> crate::Result<DeviceBytes<'s>>
381    where
382        T: 'static,
383    {
384        let inner = super::interop::retain_tensor_bytes(&self.runtime, tensor, op)?;
385        Ok(DeviceBytes {
386            inner,
387            _scope: PhantomData,
388            _not_send_sync: PhantomData,
389        })
390    }
391
392    /// Allocate a CubeCL-owned byte workspace returned as [`DeviceBytes`].
393    ///
394    /// # Errors
395    ///
396    /// Returns [`crate::Error::BackendSource`] when CubeCL cannot allocate or
397    /// inspect the workspace resource.
398    pub fn alloc_bytes(&self, nbytes: usize, op: &'static str) -> crate::Result<DeviceBytes<'s>> {
399        let inner = super::interop::alloc_device_bytes(&self.runtime, nbytes, op)?;
400        Ok(DeviceBytes {
401            inner,
402            _scope: PhantomData,
403            _not_send_sync: PhantomData,
404        })
405    }
406
407    /// Copy bytes from one device span to another on the session stream.
408    ///
409    /// Both spans must be produced by this session (via [`Session::tensor`],
410    /// [`Session::tensor_mut`], or [`Session::alloc_bytes`]) and must not
411    /// overlap; the copy is stream-ordered with other enqueued work. This is
412    /// the raw equivalent of a same-device `memcpyDeviceToDeviceAsync`.
413    ///
414    /// # Safety
415    ///
416    /// `dst` and `src` must be aligned device spans of at least `nbytes`
417    /// bytes, backed by allocations that outlive the (unsynchronized) copy;
418    /// `dst` must be uniquely owned for writing and `src` must not alias it.
419    ///
420    /// # Errors
421    ///
422    /// Returns [`crate::Error::BackendSource`] when the driver rejects the
423    /// device-to-device copy.
424    pub unsafe fn copy_bytes(
425        &self,
426        dst: *mut std::ffi::c_void,
427        src: *const std::ffi::c_void,
428        nbytes: usize,
429        op: &'static str,
430    ) -> crate::Result<()> {
431        if nbytes == 0 {
432            return Ok(());
433        }
434        let stream = self.stream as *mut cudarc::runtime::sys::CUstream_st;
435        cudarc::runtime::result::memcpy_dtod_async(dst, src, nbytes, stream)
436            .map_err(|err| crate::Error::backend_source(op, err))
437    }
438
439    /// Upload host bytes into a CubeCL-owned workspace returned as
440    /// [`DeviceBytes`].
441    ///
442    /// # Errors
443    ///
444    /// Returns [`crate::Error::BackendSource`] when CubeCL cannot upload or
445    /// inspect the workspace resource, or [`crate::Error::Validation`] when
446    /// the pointer address cannot be represented as `usize`.
447    pub fn upload_bytes(&self, bytes: &[u8], op: &'static str) -> crate::Result<DeviceBytes<'s>> {
448        let inner = super::interop::upload_device_bytes(&self.runtime, bytes, op)?;
449        Ok(DeviceBytes {
450            inner,
451            _scope: PhantomData,
452            _not_send_sync: PhantomData,
453        })
454    }
455
456    /// Read a resident tensor's bytes back to host memory.
457    ///
458    /// Synchronizes the session stream, then returns the tensor data as a
459    /// host-resident typed tensor (same shape). This is the only host barrier
460    /// introduced by the raw-session path; call it only where a value must be
461    /// inspected or returned to the host.
462    ///
463    /// # Errors
464    ///
465    /// Returns [`crate::Error::RuntimeState`] when the tensor is not resident
466    /// on this session's runtime/device, or [`crate::Error::BackendSource`]
467    /// when synchronization or readback fails.
468    pub fn download_tensor<T>(
469        &self,
470        tensor: &TypedTensor<T, impl TensorRank>,
471        op: &'static str,
472    ) -> crate::Result<TypedTensor<T>>
473    where
474        T: cubecl::prelude::CubeElement
475            + tenferro_tensor::TensorScalar
476            + Clone
477            + Send
478            + Sync
479            + 'static,
480    {
481        super::interop::download_typed_tensor(&self.runtime, tensor, op)
482    }
483
484    /// Load a CUDA module from PTX source text.
485    ///
486    /// The PTX is compiled for the current device's architecture by the
487    /// driver; on older toolchains pass PTX compiled with a compatible
488    /// `compute_XX` target. The raw session context is current, so no explicit
489    /// context switch is needed.
490    ///
491    /// # Errors
492    ///
493    /// Returns [`crate::Error::BackendSource`] when the driver rejects the
494    /// image (unsupported format, architecture mismatch).
495    pub fn load_ptx(&self, ptx: &std::ffi::CStr) -> crate::Result<Module> {
496        module::load_module_data(ptx.as_ptr().cast(), self.runtime.clone(), "raw.load_ptx")
497    }
498
499    /// Load a CUDA module from CUBIN binary data.
500    ///
501    /// CUBIN is architecture-specific; it must have been compiled for the
502    /// current device.
503    ///
504    /// # Errors
505    ///
506    /// Returns [`crate::Error::BackendSource`] when the driver rejects the
507    /// image (mismatched architecture, corrupt data).
508    pub fn load_cubin(&self, cubin: &[u8]) -> crate::Result<Module> {
509        module::load_module_data(
510            cubin.as_ptr().cast(),
511            self.runtime.clone(),
512            "raw.load_cubin",
513        )
514    }
515
516    /// Compile CUDA source to PTX on the host using NVRTC, then load it.
517    ///
518    /// The resulting module is loaded on the session's primary context.
519    ///
520    /// # Errors
521    ///
522    /// Returns [`crate::Error::BackendSource`] when NVRTC compilation or the
523    /// driver load fails, or [`crate::Error::Validation`] when the source
524    /// contains a NUL byte.
525    pub fn compile_nvrtc(&self, src: &str, opts: &NvrtcOptions) -> crate::Result<Module> {
526        let ptx = nvrtc::compile_nvrtc(src, opts)?;
527        let ptx_src = ptx.to_src();
528        // SAFETY: the freshly compiled PTX has no interior NULs (NVRTC output
529        // is a well-formed CUDA image string).
530        let cstr = std::ffi::CString::new(ptx_src).map_err(|_| {
531            crate::Error::invalid_argument("raw.compile_nvrtc", "ptx", "PTX contains NUL")
532        })?;
533        self.load_ptx(&cstr)
534    }
535
536    /// Launch a raw CUDA kernel on the session's captured stream.
537    ///
538    /// The kernel is enqueued, not synchronized; call [`Session::synchronize`]
539    /// for a host barrier.
540    ///
541    /// # Safety
542    ///
543    /// The caller must guarantee all of the following for the duration of the
544    /// launch and until the work is synchronized:
545    ///
546    /// - **ABI/arguments**: every [`KernelArg`] matches the kernel's formal
547    ///   parameter list in order (scalars of the exact width/type, device
548    ///   pointers exactly where the kernel expects them). `TensorRef` args
549    ///   that the kernel writes must be passed as the mutable form.
550    /// - **Read/write ranges**: the kernel only reads/writes within the span
551    ///   validated by the tensor's [`TensorRef`]/[`TensorMut`] and only where
552    ///   the launch geometry covers; out-of-bounds access is UB.
553    /// - **Aliasing**: no two args alias the same device memory unless the
554    ///   kernel contract permits it.
555    /// - **Liveness**: every referenced device allocation and the [`Module`]
556    ///   (via its [`Function`]) outlive the asynchronous work; the caller must
557    ///   not drop the tensors or module until after a subsequent
558    ///   [`Session::synchronize`].
559    ///
560    /// # Errors
561    ///
562    /// Returns [`crate::Error::Validation`] carrying
563    /// `ValidationError::InvalidArgument` when the launch geometry or limits
564    /// are invalid, or [`crate::Error::BackendSource`] when the driver rejects
565    /// the launch.
566    pub unsafe fn launch(
567        &self,
568        function: &Function,
569        config: LaunchConfig,
570        args: &[KernelArg<'_>],
571    ) -> crate::Result<()> {
572        module::validate_launch_config(&config)?;
573
574        // Build stable storage for every argument: scalar values live in heap
575        // boxes (the param slot points directly at the value bytes), device
576        // pointers live in heap boxes (the param slot points at the location
577        // holding the device address, i.e. pointer-to-pointer, as CUDA kernel
578        // params require). `cuLaunchKernel` copies the parameter list
579        // synchronously at enqueue, so the storage only needs to stay alive
580        // for this call.
581        let mut scalar_storage: Vec<Box<[u8]>> = Vec::with_capacity(args.len());
582        let mut ptr_storage: Vec<Box<*mut std::ffi::c_void>> = Vec::with_capacity(args.len());
583        let mut kernel_params: Vec<*mut std::ffi::c_void> = Vec::with_capacity(args.len());
584        for arg in args {
585            match arg {
586                KernelArg::Scalar(bytes) => {
587                    scalar_storage.push(bytes.clone().into_boxed_slice());
588                    // The slot was just pushed, so `len - 1` is always valid;
589                    // index instead of unwrapping to keep `launch` panic-free.
590                    let slot = &scalar_storage[scalar_storage.len() - 1];
591                    let last_ptr = slot.as_ptr() as *const std::ffi::c_void;
592                    kernel_params.push(last_ptr as *mut std::ffi::c_void);
593                }
594                KernelArg::DevicePtr(ptr, _) => {
595                    // The Box's heap allocation is stable across the move into
596                    // `ptr_storage`; the parameter slot points at the location
597                    // holding the device address (pointer-to-pointer, as CUDA
598                    // kernel params require), and is pushed as a byte address.
599                    let mut slot_box = Box::new(*ptr);
600                    let slot = (&mut *slot_box) as *mut *mut std::ffi::c_void;
601                    kernel_params.push(slot as *mut std::ffi::c_void);
602                    ptr_storage.push(slot_box);
603                }
604            }
605        }
606
607        // SAFETY: the caller upholds the ABI/liveness contract above; the
608        // function handle comes from a module retained by `function`, and the
609        // parameter storage is alive for the (synchronous) enqueue call.
610        let stream = self.stream as *mut cudarc::driver::sys::CUstream_st;
611        cudarc::driver::result::launch_kernel(
612            function.handle(),
613            (config.grid[0], config.grid[1], config.grid[2]),
614            (config.block[0], config.block[1], config.block[2]),
615            config.shared_mem_bytes,
616            stream,
617            &mut kernel_params,
618        )
619        .map_err(|err| crate::Error::backend_source("raw.launch", err))
620    }
621
622    /// Get or lazily initialize a runtime-scoped, type-keyed extension resource.
623    ///
624    /// This is the narrow public view over the existing bounded per-runtime
625    /// [`CudaExtensionCache`](super::CudaExtensionCache). The guard holds the
626    /// cache lock and serializes access; cache keys remain per exact runtime
627    /// instance (never per-device).
628    ///
629    /// # Errors
630    ///
631    /// Returns the initializer's typed error, or
632    /// [`crate::Error::RuntimeState`] when the extension cache is poisoned.
633    pub fn resource<T>(
634        &self,
635        init: impl FnOnce() -> crate::Result<T>,
636    ) -> crate::Result<CudaResourceGuard<'_, T>>
637    where
638        T: Send + 'static,
639    {
640        let guard = self.cache.get_or_try_init(init)?;
641        Ok(CudaResourceGuard { inner: guard })
642    }
643}
644
645/// Borrowed, opaque CUDA execution stream.
646///
647/// Non-copy and non-constructible. The native handle is only exposed through an
648/// explicit unsafe FFI escape; the value must not be retained past the session
649/// scope.
650pub struct StreamRef<'s> {
651    raw: u64,
652    _scope: PhantomData<&'s ()>,
653    _not_send_sync: PhantomData<Rc<()>>,
654}
655
656impl<'s> StreamRef<'s> {
657    /// Extract the native stream handle for an FFI call.
658    ///
659    /// # Safety
660    ///
661    /// The returned handle is valid only for the lifetime `'s` and only while
662    /// the session's context is current on this thread. It must not be retained
663    /// or destroyed by the caller.
664    pub unsafe fn raw_handle(&self) -> u64 {
665        self.raw
666    }
667}
668
669impl fmt::Debug for StreamRef<'_> {
670    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
671        f.debug_struct("StreamRef").finish_non_exhaustive()
672    }
673}
674
675/// Checked read-only device span of a GPU-backed tensor.
676pub struct TensorRef<'s, T> {
677    base: *mut std::ffi::c_void,
678    byte_len: usize,
679    _scope: PhantomData<&'s ()>,
680    _dtype: PhantomData<T>,
681    _not_send_sync: PhantomData<Rc<()>>,
682}
683
684impl<'s, T> TensorRef<'s, T> {
685    /// Return the validated device span in bytes.
686    pub fn byte_len(&self) -> usize {
687        self.byte_len
688    }
689
690    /// Extract the device base pointer for an FFI call.
691    ///
692    /// # Safety
693    ///
694    /// The pointer is a read-only view unless the caller's kernel contract
695    /// proves otherwise, and is valid only for lifetime `'s` for the span
696    /// `[base, base + byte_len)`. It must not be retained past the session
697    /// scope.
698    pub unsafe fn raw_ptr(&self) -> *mut std::ffi::c_void {
699        self.base
700    }
701}
702
703impl<T> fmt::Debug for TensorRef<'_, T> {
704    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
705        f.debug_struct("TensorRef")
706            .field("byte_len", &self.byte_len)
707            .finish_non_exhaustive()
708    }
709}
710
711/// Checked mutable device span of a GPU-backed tensor.
712///
713/// Only obtainable from an exclusive `&mut` tensor borrow or a fresh output.
714pub struct TensorMut<'s, T> {
715    base: *mut std::ffi::c_void,
716    byte_len: usize,
717    _scope: PhantomData<&'s ()>,
718    _dtype: PhantomData<T>,
719    _not_send_sync: PhantomData<Rc<()>>,
720}
721
722impl<'s, T> TensorMut<'s, T> {
723    /// Return the validated device span in bytes.
724    pub fn byte_len(&self) -> usize {
725        self.byte_len
726    }
727
728    /// Extract the mutable device base pointer for an FFI call.
729    ///
730    /// # Safety
731    ///
732    /// The pointer is valid only for lifetime `'s` for the span
733    /// `[base, base + byte_len)`. The caller is responsible for bounds,
734    /// aliasing, and initialization under its own kernel contract.
735    pub unsafe fn raw_ptr(&self) -> *mut std::ffi::c_void {
736        self.base
737    }
738}
739
740/// A typed argument for a raw CUDA kernel launch.
741///
742/// Either a value scalar (copied into a stable buffer) or a device pointer
743/// obtained from a validated [`TensorRef`]/[`TensorMut`] or [`DeviceBytes`].
744/// Constructing an argument is safe; the ABI contract is enforced by the
745/// caller at the `unsafe` launch site.
746#[derive(Clone)]
747pub enum KernelArg<'a> {
748    /// A scalar value encoded as an exact-size byte sequence.
749    Scalar(Vec<u8>),
750    /// A device memory address argument (in/out pointer).
751    DevicePtr(*mut std::ffi::c_void, PhantomData<&'a ()>),
752}
753
754impl<'a> KernelArg<'a> {
755    /// Build a scalar argument from an exact-size value encoding.
756    pub fn scalar(bytes: &[u8]) -> Self {
757        Self::Scalar(bytes.to_vec())
758    }
759
760    /// Build a u32 scalar parameter.
761    pub fn u32(value: u32) -> Self {
762        Self::scalar(&value.to_ne_bytes())
763    }
764
765    /// Build an i32 scalar parameter.
766    pub fn i32(value: i32) -> Self {
767        Self::scalar(&value.to_ne_bytes())
768    }
769
770    /// Build a f32 scalar parameter.
771    pub fn f32(value: f32) -> Self {
772        Self::scalar(&value.to_ne_bytes())
773    }
774
775    /// Build a u64 scalar parameter.
776    pub fn u64(value: u64) -> Self {
777        Self::scalar(&value.to_ne_bytes())
778    }
779
780    /// Build an i64 scalar parameter.
781    pub fn i64(value: i64) -> Self {
782        Self::scalar(&value.to_ne_bytes())
783    }
784
785    /// Build a f64 scalar parameter.
786    pub fn f64(value: f64) -> Self {
787        Self::scalar(&value.to_ne_bytes())
788    }
789
790    /// Build a device-pointer parameter from a read-only tensor reference.
791    ///
792    /// The kernel may only read the tensor unless the launch safety contract
793    /// states otherwise; for a write target use [`KernelArg::tensor_mut`] or
794    /// [`KernelArg::output`].
795    pub fn tensor<T>(reference: &TensorRef<'a, T>) -> Self {
796        // SAFETY: `raw_ptr` exposes the validated device span; the pointer is
797        // only used here to form the launch argument while the session scope
798        // `'a` is alive, which the borrow enforces.
799        Self::DevicePtr(unsafe { reference.raw_ptr() }, PhantomData)
800    }
801
802    /// Build a device-pointer parameter from a mutable tensor reference.
803    pub fn tensor_mut<T>(reference: &TensorMut<'a, T>) -> Self {
804        // SAFETY: exclusive `&mut` borrow guarantees no aliasing for `'a`.
805        Self::DevicePtr(unsafe { reference.raw_ptr() }, PhantomData)
806    }
807
808    /// Build a device-pointer parameter from a freshly allocated output tensor.
809    pub fn output<T>(reference: &TensorMut<'a, T>) -> Self {
810        Self::tensor_mut(reference)
811    }
812
813    /// Build a device-pointer parameter from a workspace allocation.
814    pub fn workspace(bytes: &DeviceBytes<'a>) -> Self {
815        let mut ptr = std::ptr::null_mut::<std::ffi::c_void>();
816        bytes.with_ptr(|p| ptr = p);
817        Self::DevicePtr(ptr, PhantomData)
818    }
819}
820
821/// CubeCL-owned device byte workspace kept alive for CUDA library calls.
822pub struct DeviceBytes<'s> {
823    inner: super::interop::DeviceByteBuffer,
824    _scope: PhantomData<&'s ()>,
825    _not_send_sync: PhantomData<Rc<()>>,
826}
827
828impl<'s> DeviceBytes<'s> {
829    /// Borrow the workspace device pointer for an FFI call.
830    pub fn with_ptr(&self, f: impl FnOnce(*mut std::ffi::c_void)) {
831        self.inner.with_ptr(f)
832    }
833
834    /// Return whether this workspace holds a live allocation.
835    pub fn is_empty(&self) -> bool {
836        self.inner.is_empty()
837    }
838}
839
840impl fmt::Debug for DeviceBytes<'_> {
841    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
842        f.debug_struct("DeviceBytes")
843            .field("is_empty", &self.is_empty())
844            .finish_non_exhaustive()
845    }
846}
847
848/// Runtime-scoped, type-keyed resource guard.
849///
850/// A narrow borrowed view over the bounded per-runtime extension cache.
851pub struct CudaResourceGuard<'cache, T> {
852    inner: CudaExtensionCacheGuard<'cache, T>,
853}
854
855impl<T: 'static> std::ops::Deref for CudaResourceGuard<'_, T> {
856    type Target = T;
857
858    fn deref(&self) -> &Self::Target {
859        &self.inner
860    }
861}
862
863impl<T: 'static> fmt::Debug for CudaResourceGuard<'_, T> {
864    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
865        f.debug_struct("CudaResourceGuard")
866            .field("value_type", &std::any::type_name::<T>())
867            .finish_non_exhaustive()
868    }
869}