Skip to main content

cuvs/
device.rs

1use std::{ffi::c_void, fmt, marker::PhantomData, mem, ptr::NonNull, sync::Arc};
2
3use cudf::{
4    CudfType, DenseMatrixLayout, PackedMatrix, Runtime, ValidatedPackedMatrix,
5    core::{Column, DeviceBuffer as CudfDeviceBuffer},
6    interop::contract::TypeId as InteropTypeId,
7    runtime::ExecutionBindingId,
8};
9
10use crate::{
11    error::{Result, from_cudf, invalid_input, require_nonzero},
12    resources::CuvsResources,
13};
14
15/// Element type of a cuVS device buffer or tensor.
16///
17/// Implemented for `f32`, `f64`, `i8`, `u8`, `i32`, and `i64`. The sealed supertrait
18/// [`CudfType`] restricts implementations to this crate.
19pub trait DeviceElement: CudfType + Copy + 'static {
20    /// DLPack type code used, with [`DLPACK_BITS`](Self::DLPACK_BITS) and one
21    /// lane, to describe tensors of this element to cuVS.
22    const DLPACK_CODE: u8;
23    /// DLPack element width in bits.
24    const DLPACK_BITS: u8;
25    /// RAPIDS interop type a prepared cuDF column must carry to be read as a
26    /// tensor of this element.
27    const INTEROP_TYPE: InteropTypeId;
28
29    /// Returns whether host values are valid native cuVS input.
30    ///
31    /// Floating elements require finite values; integer elements accept every value.
32    fn valid_native_input(_values: &[Self]) -> bool {
33        true
34    }
35
36    fn validate_native_buffer(_buffer: &CudfDeviceBuffer, _operation: &'static str) -> Result<()> {
37        Ok(())
38    }
39}
40
41impl DeviceElement for f32 {
42    const DLPACK_CODE: u8 = cuvs_sys::DLDataTypeCode::kDLFloat as u8;
43    const DLPACK_BITS: u8 = 32;
44    const INTEROP_TYPE: InteropTypeId = InteropTypeId::Float32;
45
46    fn valid_native_input(values: &[Self]) -> bool {
47        values.iter().all(|value| value.is_finite())
48    }
49
50    fn validate_native_buffer(buffer: &CudfDeviceBuffer, operation: &'static str) -> Result<()> {
51        buffer.validate_finite_float32().map_err(|error| from_cudf(operation, error))?;
52        Ok(())
53    }
54}
55
56impl DeviceElement for f64 {
57    const DLPACK_CODE: u8 = cuvs_sys::DLDataTypeCode::kDLFloat as u8;
58    const DLPACK_BITS: u8 = 64;
59    const INTEROP_TYPE: InteropTypeId = InteropTypeId::Float64;
60
61    fn valid_native_input(values: &[Self]) -> bool {
62        values.iter().all(|value| value.is_finite())
63    }
64
65    fn validate_native_buffer(buffer: &CudfDeviceBuffer, operation: &'static str) -> Result<()> {
66        buffer.validate_finite_float64().map_err(|error| from_cudf(operation, error))?;
67        Ok(())
68    }
69}
70
71impl DeviceElement for i8 {
72    const DLPACK_CODE: u8 = cuvs_sys::DLDataTypeCode::kDLInt as u8;
73    const DLPACK_BITS: u8 = 8;
74    const INTEROP_TYPE: InteropTypeId = InteropTypeId::Int8;
75}
76
77impl DeviceElement for u8 {
78    const DLPACK_CODE: u8 = cuvs_sys::DLDataTypeCode::kDLUInt as u8;
79    const DLPACK_BITS: u8 = 8;
80    const INTEROP_TYPE: InteropTypeId = InteropTypeId::UInt8;
81}
82
83impl DeviceElement for i32 {
84    const DLPACK_CODE: u8 = cuvs_sys::DLDataTypeCode::kDLInt as u8;
85    const DLPACK_BITS: u8 = 32;
86    const INTEROP_TYPE: InteropTypeId = InteropTypeId::Int32;
87}
88
89impl DeviceElement for i64 {
90    const DLPACK_CODE: u8 = cuvs_sys::DLDataTypeCode::kDLInt as u8;
91    const DLPACK_BITS: u8 = 64;
92    const INTEROP_TYPE: InteropTypeId = InteropTypeId::Int64;
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub enum MatrixLayout {
97    RowMajor,
98    ColumnMajor,
99}
100
101impl From<DenseMatrixLayout> for MatrixLayout {
102    fn from(layout: DenseMatrixLayout) -> Self {
103        match layout {
104            DenseMatrixLayout::RowMajor => Self::RowMajor,
105            DenseMatrixLayout::ColumnMajor => Self::ColumnMajor,
106        }
107    }
108}
109
110impl From<MatrixLayout> for DenseMatrixLayout {
111    fn from(layout: MatrixLayout) -> Self {
112        match layout {
113            MatrixLayout::RowMajor => Self::RowMajor,
114            MatrixLayout::ColumnMajor => Self::ColumnMajor,
115        }
116    }
117}
118
119/// A typed owner for a cudf runtime-allocated device buffer.
120pub struct DeviceBuffer<T: DeviceElement> {
121    inner: CudfDeviceBuffer,
122    runtime: Arc<Runtime>,
123    len: usize,
124    native_input_validated: bool,
125    _element: PhantomData<T>,
126}
127
128impl<T: DeviceElement> DeviceBuffer<T> {
129    pub fn new(resources: &CuvsResources, len: usize) -> Result<Self> {
130        let operation = "DeviceBuffer::new";
131        require_nonzero(len, operation, "len")?;
132        let size_bytes = len
133            .checked_mul(mem::size_of::<T>())
134            .ok_or_else(|| invalid_input(operation, format!("allocation size overflow for {len} elements")))?;
135        let runtime = Arc::clone(resources.runtime());
136        let inner = runtime.device_buffer(size_bytes).map_err(|error| from_cudf(operation, error))?;
137        Ok(Self { inner, runtime, len, native_input_validated: false, _element: PhantomData })
138    }
139
140    pub fn from_slice(resources: &CuvsResources, values: &[T]) -> Result<Self> {
141        let operation = "DeviceBuffer::from_slice";
142        require_nonzero(values.len(), operation, "values.len")?;
143        if !T::valid_native_input(values) {
144            return Err(invalid_input(operation, "floating native inputs must contain only finite values"));
145        }
146        let runtime = Arc::clone(resources.runtime());
147        let inner = runtime.device_buffer_from_host_data(values).map_err(|error| from_cudf(operation, error))?;
148        Ok(Self { inner, runtime, len: values.len(), native_input_validated: true, _element: PhantomData })
149    }
150
151    pub fn len(&self) -> usize {
152        self.len
153    }
154
155    pub fn is_empty(&self) -> bool {
156        self.len == 0
157    }
158
159    pub fn size_bytes(&self) -> usize {
160        self.len * mem::size_of::<T>()
161    }
162
163    pub fn binding_id(&self) -> ExecutionBindingId {
164        self.inner.binding_id()
165    }
166
167    pub fn runtime(&self) -> &Arc<Runtime> {
168        &self.runtime
169    }
170
171    pub(crate) fn native_input_validated(&self) -> bool {
172        self.native_input_validated
173    }
174
175    pub(crate) fn validate_native_input(&mut self, operation: &'static str) -> Result<()> {
176        T::validate_native_buffer(&self.inner, operation)?;
177        self.native_input_validated = true;
178        Ok(())
179    }
180
181    pub fn as_validated_tensor<'a>(
182        &'a self,
183        resources: &CuvsResources,
184        shape: &[usize],
185        strides: Option<&[usize]>,
186        byte_offset: usize,
187    ) -> Result<ValidatedDeviceTensor<'a, T>> {
188        let operation = "DeviceBuffer::as_validated_tensor";
189        if !self.native_input_validated {
190            return Err(invalid_input(operation, "device buffer has not passed input validation"));
191        }
192        self.as_tensor(resources, shape, strides, byte_offset).map(ValidatedDeviceTensor::new)
193    }
194
195    pub fn as_validated_matrix(
196        &self,
197        resources: &CuvsResources,
198        rows: usize,
199        columns: usize,
200    ) -> Result<ValidatedDeviceTensor<'_, T>> {
201        self.as_validated_tensor(resources, &[rows, columns], None, 0)
202    }
203
204    pub fn as_tensor<'a>(
205        &'a self,
206        resources: &CuvsResources,
207        shape: &[usize],
208        strides: Option<&[usize]>,
209        byte_offset: usize,
210    ) -> Result<DeviceTensor<'a, T>> {
211        let operation = "DeviceBuffer::as_tensor";
212        ensure_buffer_binding(resources, &self.inner, operation)?;
213        let data = self.inner.as_ptr().map_err(|error| from_cudf(operation, error))? as *mut c_void;
214        DeviceTensor::from_parts(
215            data,
216            resources.device().ordinal() as i32,
217            shape,
218            strides,
219            self.size_bytes(),
220            byte_offset,
221            self.binding_id(),
222            operation,
223        )
224    }
225
226    pub fn as_tensor_mut<'a>(
227        &'a mut self,
228        resources: &CuvsResources,
229        shape: &[usize],
230        strides: Option<&[usize]>,
231        byte_offset: usize,
232    ) -> Result<DeviceTensorMut<'a, T>> {
233        let operation = "DeviceBuffer::as_tensor_mut";
234        ensure_buffer_binding(resources, &self.inner, operation)?;
235        self.native_input_validated = false;
236        let size_bytes = self.size_bytes();
237        let data = self.inner.as_mut_ptr().map_err(|error| from_cudf(operation, error))?;
238        DeviceTensorMut::from_parts(
239            data,
240            resources.device().ordinal() as i32,
241            shape,
242            strides,
243            size_bytes,
244            byte_offset,
245            self.binding_id(),
246            operation,
247        )
248    }
249
250    pub fn as_matrix(&self, resources: &CuvsResources, rows: usize, columns: usize) -> Result<DeviceTensor<'_, T>> {
251        self.as_tensor(resources, &[rows, columns], None, 0)
252    }
253
254    pub fn as_matrix_mut(
255        &mut self,
256        resources: &CuvsResources,
257        rows: usize,
258        columns: usize,
259    ) -> Result<DeviceTensorMut<'_, T>> {
260        self.as_tensor_mut(resources, &[rows, columns], None, 0)
261    }
262
263    pub fn inner(&self) -> &CudfDeviceBuffer {
264        &self.inner
265    }
266
267    pub(crate) fn inner_mut(&mut self) -> &mut CudfDeviceBuffer {
268        self.native_input_validated = false;
269        &mut self.inner
270    }
271
272    pub fn into_inner(self) -> CudfDeviceBuffer {
273        self.inner
274    }
275
276    /// Adopt this allocation as one flat cudf column without copying it.
277    pub fn into_column(self) -> Result<Column> {
278        let operation = "DeviceBuffer::into_column";
279        let Self { inner, runtime, len, .. } = self;
280        runtime.column_from_device_buffer(inner, len, T::TYPE_ID).map_err(|error| from_cudf(operation, error))
281    }
282}
283
284impl DeviceBuffer<f32> {
285    pub(crate) fn validate_native_distances_for_valid_neighbors(
286        &self,
287        neighbors: &DeviceBuffer<i64>,
288        sentinel: i64,
289        operation: &'static str,
290    ) -> Result<()> {
291        self.inner
292            .validate_finite_float32_unless_int64_equals(neighbors.inner(), sentinel)
293            .map_err(|error| from_cudf(operation, error))
294    }
295}
296
297impl DeviceBuffer<i64> {
298    pub(crate) fn validate_native_index_range_or_sentinel(
299        &self,
300        min_inclusive: i64,
301        max_exclusive: i64,
302        sentinel: i64,
303        operation: &'static str,
304    ) -> Result<()> {
305        self.inner
306            .validate_int64_range_or_sentinel(min_inclusive, max_exclusive, sentinel)
307            .map_err(|error| from_cudf(operation, error))
308    }
309
310    pub(crate) fn validate_native_index_range(
311        &self,
312        min_inclusive: i64,
313        max_exclusive: i64,
314        operation: &'static str,
315    ) -> Result<()> {
316        let proof = self
317            .inner
318            .validate_int64_range(min_inclusive, max_exclusive)
319            .map_err(|error| from_cudf(operation, error))?;
320        if proof.len() != self.len {
321            return Err(invalid_input(
322                operation,
323                format!("index range validation covered {} values, expected {}", proof.len(), self.len),
324            ));
325        }
326        if proof.binding_id() != self.binding_id() {
327            return Err(invalid_input(operation, "index range validation used a different execution binding")
328                .with_context("validated_binding_id", proof.binding_id())
329                .with_context("buffer_binding_id", self.binding_id()));
330        }
331        if proof.placement().device != self.inner.device() {
332            return Err(invalid_input(operation, "index range validation used a different CUDA device")
333                .with_context("validated_device", proof.placement().device.ordinal())
334                .with_context("buffer_device", self.inner.device().ordinal()));
335        }
336        if proof.min_inclusive() != min_inclusive || proof.max_exclusive() != max_exclusive {
337            return Err(invalid_input(operation, "index validation proof covered a different value range"));
338        }
339        Ok(())
340    }
341}
342
343impl<T: DeviceElement> fmt::Debug for DeviceBuffer<T> {
344    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345        formatter
346            .debug_struct("DeviceBuffer")
347            .field("element", &std::any::type_name::<T>())
348            .field("len", &self.len)
349            .field("binding_id", &self.binding_id())
350            .finish_non_exhaustive()
351    }
352}
353
354enum MatrixStorage<T: DeviceElement> {
355    Buffer(DeviceBuffer<T>),
356    Packed(PackedMatrix),
357}
358
359/// An owned contiguous numeric matrix on one exact cudf execution binding.
360///
361/// Packed storage provides exclusive ownership, allowing in-place native
362/// updates without mutating source columns.
363pub struct OwnedMatrix<T: DeviceElement> {
364    storage: MatrixStorage<T>,
365    rows: usize,
366    columns: usize,
367    layout: MatrixLayout,
368    input_validated: bool,
369}
370
371impl<T: DeviceElement> OwnedMatrix<T> {
372    pub fn new(resources: &CuvsResources, rows: usize, columns: usize, layout: MatrixLayout) -> Result<Self> {
373        let operation = "OwnedMatrix::new";
374        require_nonzero(rows, operation, "rows")?;
375        require_nonzero(columns, operation, "columns")?;
376        let len = rows
377            .checked_mul(columns)
378            .ok_or_else(|| invalid_input(operation, "matrix element count overflowed usize"))?;
379        Ok(Self {
380            storage: MatrixStorage::Buffer(DeviceBuffer::new(resources, len)?),
381            rows,
382            columns,
383            layout,
384            input_validated: false,
385        })
386    }
387
388    pub fn from_packed(resources: &CuvsResources, matrix: PackedMatrix) -> Result<Self> {
389        let operation = "OwnedMatrix::from_packed";
390        resources.validate_exact_binding(operation)?;
391        if matrix.element() != T::TYPE_ID {
392            return Err(invalid_input(operation, "packed matrix element does not match requested cuVS element")
393                .with_context("packed_element", format!("{:?}", matrix.element()))
394                .with_context("requested_element", format!("{:?}", T::TYPE_ID)));
395        }
396        let (rows, columns, layout) = {
397            let validated = matrix.validate().map_err(|error| from_cudf(operation, error))?;
398            require_nonzero(validated.rows(), operation, "rows")?;
399            require_nonzero(validated.columns(), operation, "columns")?;
400            if validated.binding_id() != resources.binding_id() {
401                return Err(invalid_input(
402                    operation,
403                    "packed matrix and cuVS resources must use the same exact runtime binding",
404                )
405                .with_context("matrix_binding_id", validated.binding_id())
406                .with_context("resource_binding_id", resources.binding_id()));
407            }
408            if validated.device() != resources.device() {
409                return Err(invalid_input(operation, "packed matrix and cuVS resources must use the same CUDA device")
410                    .with_context("matrix_device", validated.device().ordinal())
411                    .with_context("resource_device", resources.device().ordinal()));
412            }
413            (validated.rows(), validated.columns(), validated.layout().into())
414        };
415        Ok(Self { storage: MatrixStorage::Packed(matrix), rows, columns, layout, input_validated: true })
416    }
417
418    pub(crate) fn from_buffer(
419        resources: &CuvsResources,
420        buffer: DeviceBuffer<T>,
421        rows: usize,
422        columns: usize,
423        layout: MatrixLayout,
424        operation: &'static str,
425    ) -> Result<Self> {
426        resources.validate_exact_binding(operation)?;
427        let expected = rows
428            .checked_mul(columns)
429            .ok_or_else(|| invalid_input(operation, "matrix element count overflowed usize"))?;
430        if buffer.len() != expected {
431            return Err(invalid_input(
432                operation,
433                format!("matrix buffer contains {} elements, expected {expected}", buffer.len()),
434            ));
435        }
436        ensure_buffer_binding(resources, buffer.inner(), operation)?;
437        let input_validated = buffer.native_input_validated();
438        Ok(Self { storage: MatrixStorage::Buffer(buffer), rows, columns, layout, input_validated })
439    }
440
441    pub fn rows(&self) -> usize {
442        self.rows
443    }
444
445    pub fn columns(&self) -> usize {
446        self.columns
447    }
448
449    pub fn layout(&self) -> MatrixLayout {
450        self.layout
451    }
452
453    pub fn binding_id(&self) -> ExecutionBindingId {
454        match &self.storage {
455            MatrixStorage::Buffer(buffer) => buffer.binding_id(),
456            MatrixStorage::Packed(matrix) => matrix.binding_id(),
457        }
458    }
459
460    pub fn is_input_validated(&self) -> bool {
461        self.input_validated
462    }
463
464    pub fn as_validated_tensor<'a>(&'a self, resources: &CuvsResources) -> Result<ValidatedDeviceTensor<'a, T>> {
465        let operation = "OwnedMatrix::as_validated_tensor";
466        if !self.input_validated {
467            return Err(invalid_input(operation, "owned matrix has not passed input validation"));
468        }
469        self.as_tensor(resources).map(ValidatedDeviceTensor::new)
470    }
471
472    pub(crate) fn as_tensor<'a>(&'a self, resources: &CuvsResources) -> Result<DeviceTensor<'a, T>> {
473        let operation = "OwnedMatrix::as_tensor";
474        self.validate_binding(resources, operation)?;
475        let data = match &self.storage {
476            MatrixStorage::Buffer(buffer) => {
477                buffer.inner().as_ptr().map_err(|error| from_cudf(operation, error))? as *mut c_void
478            },
479            MatrixStorage::Packed(matrix) => {
480                matrix.as_column().device_data_ptr().map_err(|error| from_cudf(operation, error))? as *mut c_void
481            },
482        };
483        self.tensor_from_ptr(data, operation)
484    }
485
486    pub(crate) fn as_tensor_mut<'a>(&'a mut self, resources: &CuvsResources) -> Result<DeviceTensorMut<'a, T>> {
487        let operation = "OwnedMatrix::as_tensor_mut";
488        self.validate_binding(resources, operation)?;
489        self.input_validated = false;
490        let data = match &mut self.storage {
491            MatrixStorage::Buffer(buffer) => {
492                buffer.inner_mut().as_mut_ptr().map_err(|error| from_cudf(operation, error))?
493            },
494            // The packed matrix is exclusively owned by `self`; no safe API
495            // can retain an alias after it was moved into this owner.
496            MatrixStorage::Packed(matrix) => {
497                matrix.as_column().device_data_ptr().map_err(|error| from_cudf(operation, error))? as *mut c_void
498            },
499        };
500        let strides = self.strides();
501        DeviceTensorMut::from_parts(
502            data,
503            resources.device().ordinal() as i32,
504            &[self.rows, self.columns],
505            strides.as_ref().map(<[usize; 2]>::as_slice),
506            self.byte_len(),
507            0,
508            self.binding_id(),
509            operation,
510        )
511    }
512
513    pub(crate) fn validate_native_output(&mut self, resources: &CuvsResources, operation: &'static str) -> Result<()> {
514        self.validate_binding(resources, operation)?;
515        match &mut self.storage {
516            MatrixStorage::Buffer(buffer) => buffer.validate_native_input(operation)?,
517            MatrixStorage::Packed(matrix) => {
518                let proof = matrix.validate().map_err(|error| from_cudf(operation, error))?;
519                if proof.len() != self.rows * self.columns {
520                    return Err(invalid_input(
521                        operation,
522                        format!(
523                            "output validation covered {} values, expected {}",
524                            proof.len(),
525                            self.rows * self.columns
526                        ),
527                    ));
528                }
529                if proof.binding_id() != resources.binding_id() {
530                    return Err(invalid_input(operation, "output validation used a different execution binding")
531                        .with_context("validated_binding_id", proof.binding_id())
532                        .with_context("resource_binding_id", resources.binding_id()));
533                }
534            },
535        }
536        self.input_validated = true;
537        Ok(())
538    }
539
540    fn validate_binding(&self, resources: &CuvsResources, operation: &'static str) -> Result<()> {
541        resources.validate_exact_binding(operation)?;
542        if self.binding_id() != resources.binding_id() {
543            return Err(invalid_input(operation, "owned matrix and cuVS resources must use the same exact binding")
544                .with_context("matrix_binding_id", self.binding_id())
545                .with_context("resource_binding_id", resources.binding_id()));
546        }
547        Ok(())
548    }
549
550    fn byte_len(&self) -> usize {
551        self.rows * self.columns * mem::size_of::<T>()
552    }
553
554    fn strides(&self) -> Option<[usize; 2]> {
555        match self.layout {
556            MatrixLayout::RowMajor => None,
557            MatrixLayout::ColumnMajor => Some([1, self.rows]),
558        }
559    }
560
561    fn tensor_from_ptr<'a>(&'a self, data: *mut c_void, operation: &'static str) -> Result<DeviceTensor<'a, T>> {
562        let strides = self.strides();
563        DeviceTensor::from_parts(
564            data,
565            self.device_ordinal(),
566            &[self.rows, self.columns],
567            strides.as_ref().map(<[usize; 2]>::as_slice),
568            self.byte_len(),
569            0,
570            self.binding_id(),
571            operation,
572        )
573    }
574
575    fn device_ordinal(&self) -> i32 {
576        match &self.storage {
577            MatrixStorage::Buffer(buffer) => buffer.inner().device().ordinal() as i32,
578            MatrixStorage::Packed(matrix) => matrix.device().ordinal() as i32,
579        }
580    }
581}
582
583impl<T: DeviceElement> fmt::Debug for OwnedMatrix<T> {
584    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
585        formatter
586            .debug_struct("OwnedMatrix")
587            .field("rows", &self.rows)
588            .field("columns", &self.columns)
589            .field("layout", &self.layout)
590            .field("binding_id", &self.binding_id())
591            .finish_non_exhaustive()
592    }
593}
594
595fn ensure_buffer_binding(resources: &CuvsResources, buffer: &CudfDeviceBuffer, operation: &'static str) -> Result<()> {
596    resources.validate_exact_binding(operation)?;
597    if buffer.binding_id() != resources.binding_id() {
598        return Err(invalid_input(
599            operation,
600            "device buffer and cuVS resources must use the same exact runtime binding",
601        )
602        .with_context("buffer_binding", format!("{:?}", buffer.binding_id()))
603        .with_context("resource_binding", format!("{:?}", resources.binding_id())));
604    }
605    if buffer.device() != resources.device() {
606        return Err(invalid_input(operation, "device buffer and cuVS resources must use the same CUDA device")
607            .with_context("buffer_device", buffer.device().ordinal())
608            .with_context("resource_device", resources.device().ordinal()));
609    }
610    Ok(())
611}
612
613#[derive(Clone, Copy)]
614pub(crate) struct TensorParts<'a, T: DeviceElement> {
615    pub(crate) data: NonNull<c_void>,
616    pub(crate) device: i32,
617    pub(crate) shape: &'a [i64],
618    pub(crate) strides: Option<&'a [i64]>,
619    pub(crate) byte_len: usize,
620    pub(crate) byte_offset: usize,
621    _element: PhantomData<T>,
622}
623
624/// A lifetime-bound, read-only CUDA tensor descriptor.
625pub struct DeviceTensor<'a, T: DeviceElement> {
626    data: NonNull<c_void>,
627    device: i32,
628    shape: Box<[i64]>,
629    strides: Option<Box<[i64]>>,
630    byte_len: usize,
631    byte_offset: usize,
632    binding_id: ExecutionBindingId,
633    _borrow: PhantomData<&'a T>,
634}
635
636impl<T: DeviceElement> DeviceTensor<'_, T> {
637    #[expect(clippy::too_many_arguments, reason = "all tensor descriptor parts are required")]
638    pub(crate) fn from_parts(
639        data: *mut c_void,
640        device: i32,
641        shape: &[usize],
642        strides: Option<&[usize]>,
643        byte_len: usize,
644        byte_offset: usize,
645        binding_id: ExecutionBindingId,
646        operation: &'static str,
647    ) -> Result<Self> {
648        let (data, shape, strides) = validate_tensor::<T>(data, shape, strides, byte_len, byte_offset, operation)?;
649        Ok(Self { data, device, shape, strides, byte_len, byte_offset, binding_id, _borrow: PhantomData })
650    }
651
652    pub fn device(&self) -> i32 {
653        self.device
654    }
655
656    pub fn shape(&self) -> &[i64] {
657        &self.shape
658    }
659
660    pub fn strides(&self) -> Option<&[i64]> {
661        self.strides.as_deref()
662    }
663
664    pub fn byte_len(&self) -> usize {
665        self.byte_len
666    }
667
668    pub fn byte_offset(&self) -> usize {
669        self.byte_offset
670    }
671
672    pub fn binding_id(&self) -> ExecutionBindingId {
673        self.binding_id
674    }
675
676    pub fn len(&self) -> usize {
677        self.shape.iter().map(|value| *value as usize).product()
678    }
679
680    pub fn is_empty(&self) -> bool {
681        self.len() == 0
682    }
683
684    pub fn rows(&self) -> Result<usize> {
685        require_matrix(self.shape(), "DeviceTensor::rows").map(|shape| shape[0] as usize)
686    }
687
688    pub fn columns(&self) -> Result<usize> {
689        require_matrix(self.shape(), "DeviceTensor::columns").map(|shape| shape[1] as usize)
690    }
691
692    pub(crate) fn parts(&self) -> TensorParts<'_, T> {
693        TensorParts {
694            data: self.data,
695            device: self.device,
696            shape: &self.shape,
697            strides: self.strides.as_deref(),
698            byte_len: self.byte_len,
699            byte_offset: self.byte_offset,
700            _element: PhantomData,
701        }
702    }
703}
704
705/// A read-only tensor with finite-scanned floating values or validated integer shape.
706pub struct ValidatedDeviceTensor<'a, T: DeviceElement> {
707    inner: DeviceTensor<'a, T>,
708}
709
710impl<'a, T: DeviceElement> ValidatedDeviceTensor<'a, T> {
711    pub fn from_packed(resources: &CuvsResources, matrix: &'a ValidatedPackedMatrix<'_>) -> Result<Self> {
712        let operation = "ValidatedDeviceTensor::from_packed";
713        resources.validate_exact_binding(operation)?;
714        if matrix.element() != T::TYPE_ID {
715            return Err(invalid_input(operation, "packed matrix element does not match requested cuVS element")
716                .with_context("packed_element", format!("{:?}", matrix.element()))
717                .with_context("requested_element", format!("{:?}", T::TYPE_ID)));
718        }
719        if matrix.binding_id() != resources.binding_id() {
720            return Err(invalid_input(
721                operation,
722                "packed matrix and cuVS resources must use the same exact runtime binding",
723            )
724            .with_context("tensor_binding_id", matrix.binding_id())
725            .with_context("resource_binding_id", resources.binding_id()));
726        }
727        if matrix.device() != resources.device() {
728            return Err(invalid_input(operation, "packed matrix and cuVS resources must use the same CUDA device")
729                .with_context("tensor_device", matrix.device().ordinal())
730                .with_context("resource_device", resources.device().ordinal()));
731        }
732        let data = matrix.as_ptr().map_err(|error| from_cudf(operation, error))? as *mut c_void;
733        let strides = match MatrixLayout::from(matrix.layout()) {
734            MatrixLayout::RowMajor => None,
735            MatrixLayout::ColumnMajor => Some([1, matrix.rows()]),
736        };
737        DeviceTensor::from_parts(
738            data,
739            resources.device().ordinal() as i32,
740            &[matrix.rows(), matrix.columns()],
741            strides.as_ref().map(<[usize; 2]>::as_slice),
742            matrix.len() * mem::size_of::<T>(),
743            0,
744            matrix.binding_id(),
745            operation,
746        )
747        .map(Self::new)
748    }
749
750    pub(crate) fn new(inner: DeviceTensor<'a, T>) -> Self {
751        Self { inner }
752    }
753
754    pub(crate) fn as_tensor(&self) -> &DeviceTensor<'a, T> {
755        &self.inner
756    }
757
758    pub fn device(&self) -> i32 {
759        self.inner.device()
760    }
761
762    pub fn shape(&self) -> &[i64] {
763        self.inner.shape()
764    }
765
766    pub fn strides(&self) -> Option<&[i64]> {
767        self.inner.strides()
768    }
769
770    pub fn binding_id(&self) -> ExecutionBindingId {
771        self.inner.binding_id()
772    }
773
774    pub fn rows(&self) -> Result<usize> {
775        self.inner.rows()
776    }
777
778    pub fn columns(&self) -> Result<usize> {
779        self.inner.columns()
780    }
781}
782
783impl<T: DeviceElement> fmt::Debug for ValidatedDeviceTensor<'_, T> {
784    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
785        formatter.debug_tuple("ValidatedDeviceTensor").field(&self.inner).finish()
786    }
787}
788
789impl<T: DeviceElement> fmt::Debug for DeviceTensor<'_, T> {
790    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
791        formatter
792            .debug_struct("DeviceTensor")
793            .field("element", &std::any::type_name::<T>())
794            .field("device", &self.device)
795            .field("shape", &self.shape)
796            .field("strides", &self.strides)
797            .field("byte_len", &self.byte_len)
798            .field("byte_offset", &self.byte_offset)
799            .field("binding_id", &self.binding_id)
800            .finish_non_exhaustive()
801    }
802}
803
804/// A lifetime-bound, exclusive CUDA tensor descriptor.
805pub struct DeviceTensorMut<'a, T: DeviceElement> {
806    inner: DeviceTensor<'a, T>,
807    _exclusive: PhantomData<&'a mut T>,
808}
809
810impl<T: DeviceElement> DeviceTensorMut<'_, T> {
811    #[expect(clippy::too_many_arguments, reason = "all tensor descriptor parts are required")]
812    fn from_parts(
813        data: *mut c_void,
814        device: i32,
815        shape: &[usize],
816        strides: Option<&[usize]>,
817        byte_len: usize,
818        byte_offset: usize,
819        binding_id: ExecutionBindingId,
820        operation: &'static str,
821    ) -> Result<Self> {
822        Ok(Self {
823            inner: DeviceTensor::from_parts(
824                data,
825                device,
826                shape,
827                strides,
828                byte_len,
829                byte_offset,
830                binding_id,
831                operation,
832            )?,
833            _exclusive: PhantomData,
834        })
835    }
836
837    pub fn as_const(&self) -> DeviceTensor<'_, T> {
838        DeviceTensor {
839            data: self.inner.data,
840            device: self.inner.device,
841            shape: self.inner.shape.clone(),
842            strides: self.inner.strides.clone(),
843            byte_len: self.inner.byte_len,
844            byte_offset: self.inner.byte_offset,
845            binding_id: self.inner.binding_id,
846            _borrow: PhantomData,
847        }
848    }
849
850    pub fn device(&self) -> i32 {
851        self.inner.device()
852    }
853
854    pub fn shape(&self) -> &[i64] {
855        self.inner.shape()
856    }
857
858    pub fn strides(&self) -> Option<&[i64]> {
859        self.inner.strides()
860    }
861
862    pub fn byte_len(&self) -> usize {
863        self.inner.byte_len()
864    }
865
866    pub fn byte_offset(&self) -> usize {
867        self.inner.byte_offset()
868    }
869
870    pub fn binding_id(&self) -> ExecutionBindingId {
871        self.inner.binding_id()
872    }
873
874    pub fn len(&self) -> usize {
875        self.inner.len()
876    }
877
878    pub fn is_empty(&self) -> bool {
879        self.inner.is_empty()
880    }
881
882    pub fn rows(&self) -> Result<usize> {
883        self.inner.rows()
884    }
885
886    pub fn columns(&self) -> Result<usize> {
887        self.inner.columns()
888    }
889
890    pub(crate) fn parts(&self) -> TensorParts<'_, T> {
891        self.inner.parts()
892    }
893}
894
895impl<T: DeviceElement> fmt::Debug for DeviceTensorMut<'_, T> {
896    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
897        self.inner.fmt(formatter)
898    }
899}
900
901type ValidatedTensorParts = (NonNull<c_void>, Box<[i64]>, Option<Box<[i64]>>);
902
903fn validate_tensor<T: DeviceElement>(
904    data: *mut c_void,
905    shape: &[usize],
906    strides: Option<&[usize]>,
907    byte_len: usize,
908    byte_offset: usize,
909    operation: &'static str,
910) -> Result<ValidatedTensorParts> {
911    if shape.is_empty() {
912        return Err(invalid_input(operation, "tensor rank must be greater than zero"));
913    }
914    if strides.is_some_and(|strides| strides.len() != shape.len()) {
915        return Err(invalid_input(operation, "tensor strides must have the same rank as shape"));
916    }
917    if !byte_offset.is_multiple_of(mem::align_of::<T>()) {
918        return Err(invalid_input(operation, "tensor byte offset is not aligned for its element type"));
919    }
920
921    let mut logical_elements = 1usize;
922    let mut shape_i64 = Vec::with_capacity(shape.len());
923    for &dimension in shape {
924        require_nonzero(dimension, operation, "shape dimension")?;
925        logical_elements = logical_elements
926            .checked_mul(dimension)
927            .ok_or_else(|| invalid_input(operation, "tensor logical element count overflowed usize"))?;
928        shape_i64.push(i64::try_from(dimension).map_err(|_| invalid_input(operation, "tensor dimension exceeds i64"))?);
929    }
930
931    let strides_i64 = strides
932        .map(|strides| {
933            strides
934                .iter()
935                .map(|&stride| {
936                    require_nonzero(stride, operation, "stride")?;
937                    i64::try_from(stride).map_err(|_| invalid_input(operation, "tensor stride exceeds i64"))
938                })
939                .collect::<Result<Vec<_>>>()
940                .map(Vec::into_boxed_slice)
941        })
942        .transpose()?;
943    let last_element = match strides {
944        Some(strides) => shape.iter().zip(strides).try_fold(0usize, |offset, (&dimension, &stride)| {
945            offset
946                .checked_add(
947                    (dimension - 1)
948                        .checked_mul(stride)
949                        .ok_or_else(|| invalid_input(operation, "tensor stride footprint overflowed usize"))?,
950                )
951                .ok_or_else(|| invalid_input(operation, "tensor stride footprint overflowed usize"))
952        })?,
953        None => logical_elements - 1,
954    };
955    let footprint = last_element
956        .checked_add(1)
957        .and_then(|elements| elements.checked_mul(mem::size_of::<T>()))
958        .and_then(|bytes| byte_offset.checked_add(bytes))
959        .ok_or_else(|| invalid_input(operation, "tensor byte footprint overflowed usize"))?;
960    if footprint > byte_len {
961        return Err(invalid_input(
962            operation,
963            format!("tensor footprint {footprint} bytes exceeds backing buffer length {byte_len}"),
964        ));
965    }
966    let data =
967        NonNull::new(data).ok_or_else(|| invalid_input(operation, "non-empty CUDA tensor data must not be null"))?;
968    Ok((data, shape_i64.into_boxed_slice(), strides_i64))
969}
970
971pub(crate) fn require_matrix<'a>(shape: &'a [i64], operation: &'static str) -> Result<&'a [i64]> {
972    if shape.len() != 2 {
973        return Err(invalid_input(operation, format!("expected a rank-2 matrix, found rank {}", shape.len())));
974    }
975    Ok(shape)
976}
977
978#[cfg(test)]
979mod tests {
980    use super::*;
981    use crate::ErrorKind;
982
983    fn resources() -> (Arc<Runtime>, CuvsResources) {
984        let runtime = Arc::new(Runtime::new().expect("runtime"));
985        let resources = CuvsResources::for_runtime(Arc::clone(&runtime)).expect("cuVS resources");
986        (runtime, resources)
987    }
988
989    #[test]
990    fn native_output_validation_rejects_non_finite_buffer_before_restoring_proof() {
991        let (runtime, resources) = resources();
992        let inner = runtime.device_buffer_from_host_data(&[f32::INFINITY]).expect("raw native output");
993        let buffer =
994            DeviceBuffer::<f32> { inner, runtime, len: 1, native_input_validated: false, _element: PhantomData };
995        let mut matrix =
996            OwnedMatrix::from_buffer(&resources, buffer, 1, 1, MatrixLayout::RowMajor, "test").expect("matrix");
997
998        let error = matrix
999            .validate_native_output(&resources, "test")
1000            .expect_err("non-finite native output must not restore the finite-input proof");
1001        assert_eq!(error.kind(), ErrorKind::InvalidInput);
1002        assert!(!matrix.is_input_validated());
1003    }
1004
1005    #[test]
1006    fn float64_native_output_rejects_non_finite_values() {
1007        let (runtime, _resources) = resources();
1008        let inner = runtime.device_buffer_from_host_data(&[f64::NAN]).expect("raw native output");
1009        let mut buffer =
1010            DeviceBuffer::<f64> { inner, runtime, len: 1, native_input_validated: false, _element: PhantomData };
1011        let error = buffer.validate_native_input("test").expect_err("non-finite Float64 output must be rejected");
1012        assert_eq!(error.kind(), ErrorKind::InvalidInput);
1013        assert!(!buffer.native_input_validated());
1014    }
1015
1016    #[test]
1017    fn tensor_stride_footprint_overflow_returns_an_error() {
1018        let half_max = usize::MAX / 2;
1019        let error = validate_tensor::<f32>(
1020            NonNull::<f32>::dangling().as_ptr().cast(),
1021            &[2, 2, 2],
1022            Some(&[half_max, half_max, 1]),
1023            usize::MAX,
1024            0,
1025            "test",
1026        )
1027        .expect_err("an unrepresentable strided footprint must not panic or wrap");
1028
1029        assert_eq!(error.kind(), ErrorKind::InvalidInput);
1030        assert!(error.message().contains("footprint overflowed"));
1031    }
1032}