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
15pub trait DeviceElement: CudfType + Copy + 'static {
20 const DLPACK_CODE: u8;
23 const DLPACK_BITS: u8;
25 const INTEROP_TYPE: InteropTypeId;
28
29 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
119pub 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 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
359pub 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 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
624pub 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
705pub 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
804pub 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}