Skip to main content

singe_cutensor/
context.rs

1use std::{mem::ManuallyDrop, path::Path, ptr, sync::Arc};
2
3use singe_core::path_to_cstring;
4use singe_cuda::{context::Context as CudaContext, stream::Stream};
5
6use crate::{
7    error::{Error, Result},
8    sys, try_ffi,
9    types::{LoggerLevel, LoggerMask},
10    utility::to_u32,
11};
12
13/// A stateful cuTENSOR handle.
14///
15/// Use one context per host thread or concurrent task. The handle is movable
16/// between threads, but it is intentionally not `Clone` or `Sync`.
17#[derive(Debug)]
18pub struct Context {
19    handle: Handle,
20}
21
22#[derive(Debug)]
23struct Handle {
24    raw: sys::cutensorHandle_t,
25    cuda_ctx: Arc<CudaContext>,
26}
27
28#[derive(Debug, Clone)]
29pub(crate) struct ContextRef {
30    raw: sys::cutensorHandle_t,
31    cuda_ctx: Arc<CudaContext>,
32}
33
34// cuTENSOR handles own mutable plan-cache state. The owning wrapper and
35// lightweight references may move between threads, but are not shared as Sync.
36unsafe impl Send for Handle {}
37unsafe impl Send for ContextRef {}
38
39impl Context {
40    /// Initializes the cuTENSOR library and allocates the memory for the library context.
41    ///
42    /// The device associated with a particular cuTENSOR handle is assumed to remain unchanged after the [`Context::create`] call.
43    /// To use a different device, make that device current through the CUDA wrapper crate and create a new cuTENSOR handle with [`Context::create`].
44    ///
45    /// Each handle has a plan cache that stores least-recently-used cuTENSOR plans.
46    /// Its default capacity is 64, but it can be changed with [`Context::resize_plan_cache`].
47    /// See the Plan Cache Guide for more information.
48    ///
49    /// The returned [`Context`] frees the cuTENSOR handle when dropped.
50    ///
51    /// This blocking call is thread-safe but not reentrant.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if the CUDA context cannot be bound, if cuTENSOR cannot
56    /// create a handle, or if cuTENSOR returns a null handle.
57    pub fn create(cuda_ctx: &Arc<CudaContext>) -> Result<Self> {
58        cuda_ctx.bind()?;
59
60        let mut handle = ptr::null_mut();
61        unsafe {
62            try_ffi!(sys::cutensorCreate(&raw mut handle))?;
63        }
64
65        if handle.is_null() {
66            return Err(Error::NullHandle);
67        }
68
69        Ok(Self {
70            handle: Handle {
71                raw: handle,
72                cuda_ctx: Arc::clone(cuda_ctx),
73            },
74        })
75    }
76
77    /// Wraps an existing cuTENSOR handle and takes ownership of it.
78    ///
79    /// # Safety
80    ///
81    /// `handle` must be a valid cuTENSOR handle associated with `cuda_ctx`.
82    /// Ownership of `handle` is transferred to the returned context, and the
83    /// handle must not be destroyed elsewhere after calling this function.
84    pub unsafe fn from_raw(
85        handle: sys::cutensorHandle_t,
86        cuda_ctx: Arc<CudaContext>,
87    ) -> Result<Self> {
88        if handle.is_null() {
89            return Err(Error::NullHandle);
90        }
91
92        Ok(Self {
93            handle: Handle {
94                raw: handle,
95                cuda_ctx,
96            },
97        })
98    }
99
100    pub fn cuda_context(&self) -> &Arc<CudaContext> {
101        &self.handle.cuda_ctx
102    }
103
104    pub fn bind(&self) -> Result<()> {
105        Ok(self.cuda_context().bind()?)
106    }
107
108    pub(crate) fn as_context_ref(&self) -> ContextRef {
109        ContextRef {
110            raw: self.handle.raw,
111            cuda_ctx: Arc::clone(&self.handle.cuda_ctx),
112        }
113    }
114
115    /// Resizes the plan cache.
116    ///
117    /// Changes the number of plans that can be stored in the plan cache of the handle.
118    ///
119    /// Resizing invalidates the cache.
120    ///
121    /// The call is not thread-safe, but the resulting cache can be shared across threads safely.
122    ///
123    /// This non-blocking call is neither reentrant nor thread-safe.
124    ///
125    /// # Errors
126    ///
127    /// Returns an error if the context cannot be bound, `entry_count` cannot
128    /// be represented for cuTENSOR, or cuTENSOR rejects the new cache size.
129    pub fn resize_plan_cache(&self, entry_count: usize) -> Result<()> {
130        self.bind()?;
131
132        let entry_count = to_u32(entry_count, "entry_count")?;
133
134        unsafe {
135            try_ffi!(sys::cutensorHandleResizePlanCache(
136                self.as_raw(),
137                entry_count,
138            ))?;
139        }
140        Ok(())
141    }
142
143    /// Writes the plan cache that belongs to this handle to file.
144    ///
145    /// This non-blocking call is thread-safe but not reentrant.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the context cannot be bound, `path` cannot be
150    /// converted to a C string, no plan cache is attached, or cuTENSOR cannot
151    /// write the file.
152    pub fn write_plan_cache_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
153        self.bind()?;
154
155        let path = path_to_cstring(path.as_ref())?;
156        unsafe {
157            try_ffi!(sys::cutensorHandleWritePlanCacheToFile(
158                self.as_raw(),
159                path.as_ptr(),
160            ))?;
161        }
162        Ok(())
163    }
164
165    /// Reads a plan cache from file and overwrites this handle's cache lines.
166    ///
167    /// A cache is only valid for the same cuTENSOR version, CUDA version, and GPU
168    /// architecture, including multiprocessor count.
169    ///
170    /// This non-blocking call is thread-safe but not reentrant.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the context cannot be bound, `path` cannot be
175    /// converted to a C string, the file cannot be read, the stored cache is
176    /// incompatible with this cuTENSOR/CUDA/device configuration, the current
177    /// cache is too small for the stored data, or cuTENSOR rejects the cache.
178    pub fn read_plan_cache_from_file(&self, path: impl AsRef<Path>) -> Result<u32> {
179        self.bind()?;
180
181        let path = path_to_cstring(path.as_ref())?;
182        let mut cachelines_read = 0;
183        unsafe {
184            try_ffi!(sys::cutensorHandleReadPlanCacheFromFile(
185                self.as_raw(),
186                path.as_ptr(),
187                &raw mut cachelines_read,
188            ))?;
189        }
190        Ok(cachelines_read)
191    }
192
193    /// Writes the per-library kernel cache to file.
194    ///
195    /// Writes the just-in-time compiled kernels to the provided file.
196    /// These kernels belong to the library, not to the handle.
197    ///
198    /// This non-blocking call is thread-safe but not reentrant.
199    ///
200    /// # Errors
201    ///
202    /// Returns an error if the context cannot be bound, `path` cannot be
203    /// converted to a C string, JIT kernel caching is unsupported for this
204    /// operating system, CUDA Toolkit, or device, or cuTENSOR cannot write the
205    /// file.
206    pub fn write_kernel_cache_to_file(&self, path: impl AsRef<Path>) -> Result<()> {
207        self.bind()?;
208
209        let path = path_to_cstring(path.as_ref())?;
210        unsafe {
211            try_ffi!(sys::cutensorWriteKernelCacheToFile(
212                self.as_raw(),
213                path.as_ptr()
214            ))?;
215        }
216        Ok(())
217    }
218
219    /// Reads a kernel cache from file and adds all non-existing JIT compiled kernels to the kernel cache.
220    ///
221    /// A cache is only valid for the same cuTENSOR version, CUDA version, and GPU
222    /// architecture, including multiprocessor count.
223    ///
224    /// This non-blocking call is thread-safe but not reentrant.
225    ///
226    /// # Errors
227    ///
228    /// Returns an error if the context cannot be bound, `path` cannot be
229    /// converted to a C string, the file cannot be read, the stored cache is
230    /// incompatible with this cuTENSOR/CUDA/device configuration, JIT kernel
231    /// caching is unsupported for this operating system, CUDA Toolkit, or
232    /// device, or cuTENSOR rejects the cache.
233    pub fn read_kernel_cache_from_file(&self, path: impl AsRef<Path>) -> Result<()> {
234        self.bind()?;
235
236        let path = path_to_cstring(path.as_ref())?;
237        unsafe {
238            try_ffi!(sys::cutensorReadKernelCacheFromFile(
239                self.as_raw(),
240                path.as_ptr()
241            ))?;
242        }
243        Ok(())
244    }
245
246    pub fn ensure_stream(&self, stream: &Stream) -> Result<()> {
247        if self.cuda_context().as_ref() != stream.context() {
248            return Err(Error::StreamContextMismatch);
249        }
250
251        self.bind()
252    }
253
254    /// Sets the logging callback.
255    ///
256    /// # Safety
257    ///
258    /// `callback` must remain valid for every later cuTENSOR log event that may
259    /// call it and must follow cuTENSOR's callback requirements.
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if cuTENSOR rejects the callback.
264    pub unsafe fn set_logger_callback(callback: sys::cutensorLoggerCallback_t) -> Result<()> {
265        unsafe {
266            try_ffi!(sys::cutensorLoggerSetCallback(callback))?;
267        }
268        Ok(())
269    }
270
271    /// Sets the logging level.
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if cuTENSOR rejects the logging level.
276    pub fn set_logger_level(level: LoggerLevel) -> Result<()> {
277        unsafe {
278            try_ffi!(sys::cutensorLoggerSetLevel(level.as_raw()))?;
279        }
280        Ok(())
281    }
282
283    /// Sets the log mask.
284    ///
285    /// # Errors
286    ///
287    /// Returns an error if cuTENSOR rejects the logging mask.
288    pub fn set_logger_mask(mask: LoggerMask) -> Result<()> {
289        unsafe {
290            try_ffi!(sys::cutensorLoggerSetMask(mask.bits()))?;
291        }
292        Ok(())
293    }
294
295    /// Sets the logging output file.
296    ///
297    /// # Safety
298    ///
299    /// `file` must be a valid C `FILE` pointer for every later cuTENSOR log write
300    /// that may use it, or a null pointer if cuTENSOR accepts that for the active
301    /// logger configuration.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if cuTENSOR rejects the file pointer.
306    pub unsafe fn set_logger_file(file: *mut sys::FILE) -> Result<()> {
307        unsafe {
308            try_ffi!(sys::cutensorLoggerSetFile(file))?;
309        }
310        Ok(())
311    }
312
313    /// Sets the logging output file by path.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if `path` cannot be converted to a C string or if
318    /// cuTENSOR cannot open the log file.
319    pub fn set_logger_path(path: impl AsRef<Path>) -> Result<()> {
320        let path = path_to_cstring(path.as_ref())?;
321        unsafe {
322            try_ffi!(sys::cutensorLoggerOpenFile(path.as_ptr()))?;
323        }
324        Ok(())
325    }
326
327    /// Disables logging for the entire run.
328    ///
329    /// # Errors
330    ///
331    /// Returns an error if cuTENSOR cannot disable logging.
332    pub fn disable_logger() -> Result<()> {
333        unsafe {
334            try_ffi!(sys::cutensorLoggerForceDisable())?;
335        }
336        Ok(())
337    }
338
339    /// Returns the raw cuTENSOR handle.
340    ///
341    /// The returned handle is borrowed and remains valid only while this
342    /// context and its underlying CUDA context are alive.
343    pub fn as_raw(&self) -> sys::cutensorHandle_t {
344        self.handle.raw
345    }
346
347    /// Consumes the context and returns the raw cuTENSOR handle without
348    /// destroying it.
349    ///
350    /// The caller becomes responsible for eventually destroying the returned
351    /// handle with cuTENSOR.
352    pub fn into_raw(self) -> sys::cutensorHandle_t {
353        let context = ManuallyDrop::new(self);
354        context.handle.raw
355    }
356}
357
358impl ContextRef {
359    pub fn cuda_context(&self) -> &Arc<CudaContext> {
360        &self.cuda_ctx
361    }
362
363    pub fn bind(&self) -> Result<()> {
364        Ok(self.cuda_ctx.bind()?)
365    }
366
367    pub fn ensure_stream(&self, stream: &Stream) -> Result<()> {
368        if self.cuda_context().as_ref() != stream.context() {
369            return Err(Error::StreamContextMismatch);
370        }
371
372        self.bind()
373    }
374
375    pub(crate) fn same_handle(&self, other: &Self) -> bool {
376        self.raw == other.raw
377    }
378
379    /// Returns the raw cuTENSOR handle.
380    ///
381    /// The returned handle is borrowed and remains valid only while this
382    /// context reference and its underlying CUDA context are alive.
383    pub fn as_raw(&self) -> sys::cutensorHandle_t {
384        self.raw
385    }
386}
387
388pub(crate) fn validate_same_context(
389    expected: &ContextRef,
390    actual: &ContextRef,
391    name: &str,
392) -> Result<()> {
393    if !expected.same_handle(actual) {
394        return Err(Error::ContextMismatch { name: name.into() });
395    }
396    Ok(())
397}
398
399impl Drop for Handle {
400    fn drop(&mut self) {
401        if let Err(err) = self.cuda_ctx.bind() {
402            #[cfg(debug_assertions)]
403            eprintln!("failed to bind cuda context before destroying cutensor handle: {err}");
404        }
405
406        unsafe {
407            if let Err(err) = try_ffi!(sys::cutensorDestroy(self.raw)) {
408                #[cfg(debug_assertions)]
409                eprintln!("failed to destroy cutensor context: {err}");
410            }
411        }
412    }
413}