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}