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}